@manablox/db 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/index-BLAkQMJT.d.ts +877 -0
  2. package/dist/index-DrNMGM9N.d.ts +5193 -0
  3. package/dist/index.d.ts +123 -0
  4. package/dist/index.js +60 -0
  5. package/dist/repositories-pz4NeWaF.js +1928 -0
  6. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  7. package/dist/schema-Dm3RcBst.js +648 -0
  8. package/dist/schema.d.ts +2 -0
  9. package/dist/schema.js +2 -0
  10. package/dist/testing.d.ts +77 -0
  11. package/dist/testing.js +217 -0
  12. package/migrations/0009_audit-log.sql +40 -0
  13. package/migrations/0010_notifications-approvals.sql +45 -0
  14. package/migrations/meta/0009_snapshot.json +3192 -0
  15. package/migrations/meta/0010_snapshot.json +3574 -0
  16. package/migrations/meta/_journal.json +14 -0
  17. package/package.json +18 -10
  18. package/drizzle.config.ts +0 -11
  19. package/src/bootstrap.ts +0 -13
  20. package/src/cli/create-db.ts +0 -30
  21. package/src/cli/migrate.ts +0 -17
  22. package/src/client.ts +0 -44
  23. package/src/columns.ts +0 -39
  24. package/src/errors.ts +0 -50
  25. package/src/index.ts +0 -19
  26. package/src/migrate.ts +0 -21
  27. package/src/pagination.ts +0 -52
  28. package/src/query.ts +0 -213
  29. package/src/repositories/asset-usage.ts +0 -166
  30. package/src/repositories/asset.ts +0 -181
  31. package/src/repositories/content-type.ts +0 -116
  32. package/src/repositories/content.ts +0 -811
  33. package/src/repositories/index.ts +0 -40
  34. package/src/repositories/menu.ts +0 -235
  35. package/src/repositories/role.ts +0 -85
  36. package/src/repositories/space.ts +0 -83
  37. package/src/repositories/user.ts +0 -280
  38. package/src/repositories/webhook.ts +0 -46
  39. package/src/repositories/workflow.ts +0 -306
  40. package/src/schema/assets.ts +0 -108
  41. package/src/schema/auth.ts +0 -166
  42. package/src/schema/content-types.ts +0 -31
  43. package/src/schema/content.ts +0 -133
  44. package/src/schema/index.ts +0 -38
  45. package/src/schema/menus.ts +0 -61
  46. package/src/schema/relations.ts +0 -64
  47. package/src/schema/spaces.ts +0 -20
  48. package/src/schema/webhooks.ts +0 -46
  49. package/src/schema/workflows.ts +0 -92
  50. package/src/testing-fixtures.ts +0 -139
  51. package/src/testing.ts +0 -105
  52. package/test/asset-usage.test.ts +0 -101
  53. package/test/menu.test.ts +0 -126
  54. package/test/publish.test.ts +0 -130
  55. package/test/query.test.ts +0 -170
  56. package/test/role.test.ts +0 -81
  57. package/test/tree.test.ts +0 -188
  58. package/test/user.test.ts +0 -126
  59. package/test/webhook.test.ts +0 -48
  60. package/tsconfig.json +0 -4
  61. package/vitest.config.ts +0 -10
@@ -1,166 +0,0 @@
1
- import { and, eq, inArray, notInArray, sql } from 'drizzle-orm';
2
- import type { Database } from '../client.js';
3
- import { assetUsages, contents, publishedContents } from '../schema/index.js';
4
-
5
- /**
6
- * The asset → document reachability index.
7
- *
8
- * `published` tracks the *published projection*, not the draft: a draft that adds an
9
- * image does not make that image public, and a draft that removes one does not make it
10
- * private until the change is published. Every method below preserves that distinction,
11
- * which is why the column exists rather than the table simply holding published rows.
12
- */
13
- export class AssetUsageRepository {
14
- constructor(private readonly db: Database) {}
15
-
16
- /** The subset of `assetIds` reachable from at least one published document. */
17
- async filterPublished(assetIds: string[]): Promise<Set<string>> {
18
- if (assetIds.length === 0) return new Set();
19
- const rows = await this.db
20
- .selectDistinct({ assetId: assetUsages.assetId })
21
- .from(assetUsages)
22
- .where(and(inArray(assetUsages.assetId, assetIds), eq(assetUsages.published, true)));
23
- return new Set(rows.map((row) => row.assetId));
24
- }
25
-
26
- async forContent(contentId: string): Promise<Array<{ assetId: string; published: boolean }>> {
27
- return this.db
28
- .select({ assetId: assetUsages.assetId, published: assetUsages.published })
29
- .from(assetUsages)
30
- .where(eq(assetUsages.contentId, contentId));
31
- }
32
-
33
- /**
34
- * Records what a *draft* references.
35
- *
36
- * Rows the draft dropped are removed only if they are not currently published —
37
- * otherwise editing a draft would silently revoke access to an image the live page is
38
- * still showing.
39
- */
40
- async recordDraft(contentId: string, spaceId: string, assetIds: string[]): Promise<void> {
41
- const unique = [...new Set(assetIds)];
42
-
43
- await this.db.transaction(async (tx) => {
44
- await tx
45
- .delete(assetUsages)
46
- .where(
47
- and(
48
- eq(assetUsages.contentId, contentId),
49
- eq(assetUsages.published, false),
50
- ...(unique.length > 0 ? [notInArray(assetUsages.assetId, unique)] : []),
51
- ),
52
- );
53
-
54
- if (unique.length === 0) return;
55
-
56
- await tx
57
- .insert(assetUsages)
58
- .values(
59
- unique.map((assetId) => ({
60
- assetId,
61
- contentId,
62
- spaceId,
63
- published: false,
64
- updatedAt: new Date(),
65
- })),
66
- )
67
- // A row that is already published stays published: this write describes the
68
- // draft, and `onConflictDoNothing` is the difference between recording a draft
69
- // and revoking a live asset.
70
- .onConflictDoNothing();
71
- });
72
- }
73
-
74
- /**
75
- * Records what the published projection references.
76
- *
77
- * Assets the new revision no longer uses lose their published flag but keep their row
78
- * when the draft still references them, so the admin's "where is this used" view stays
79
- * complete.
80
- */
81
- async recordPublished(contentId: string, spaceId: string, assetIds: string[]): Promise<void> {
82
- const unique = [...new Set(assetIds)];
83
-
84
- await this.db.transaction(async (tx) => {
85
- await tx
86
- .update(assetUsages)
87
- .set({ published: false, updatedAt: new Date() })
88
- .where(
89
- and(
90
- eq(assetUsages.contentId, contentId),
91
- ...(unique.length > 0 ? [notInArray(assetUsages.assetId, unique)] : []),
92
- ),
93
- );
94
-
95
- if (unique.length === 0) return;
96
-
97
- await tx
98
- .insert(assetUsages)
99
- .values(
100
- unique.map((assetId) => ({
101
- assetId,
102
- contentId,
103
- spaceId,
104
- published: true,
105
- updatedAt: new Date(),
106
- })),
107
- )
108
- .onConflictDoUpdate({
109
- target: [assetUsages.assetId, assetUsages.contentId],
110
- set: { published: true, updatedAt: new Date() },
111
- });
112
- });
113
- }
114
-
115
- /** Unpublishing revokes every asset this document was keeping public. */
116
- async clearPublished(contentId: string): Promise<void> {
117
- await this.db
118
- .update(assetUsages)
119
- .set({ published: false, updatedAt: new Date() })
120
- .where(eq(assetUsages.contentId, contentId));
121
- }
122
-
123
- async deleteForContent(contentId: string): Promise<void> {
124
- await this.db.delete(assetUsages).where(eq(assetUsages.contentId, contentId));
125
- }
126
-
127
- async count(): Promise<number> {
128
- const rows = await this.db.select({ count: sql<number>`count(*)::int` }).from(assetUsages);
129
- return rows[0]?.count ?? 0;
130
- }
131
-
132
- /**
133
- * Every document with its draft and published field values, for the backfill.
134
- *
135
- * References are derived from field-type definitions rather than stored, so the
136
- * backfill cannot be a SQL migration — it has to run inside the application.
137
- */
138
- async backfillSource(): Promise<
139
- Array<{
140
- id: string;
141
- spaceId: string;
142
- typeId: string;
143
- draftFields: Record<string, unknown>;
144
- publishedFields: Record<string, unknown> | null;
145
- }>
146
- > {
147
- const rows = await this.db
148
- .select({
149
- id: contents.id,
150
- spaceId: contents.spaceId,
151
- typeId: contents.typeId,
152
- draftFields: contents.fields,
153
- publishedFields: publishedContents.fields,
154
- })
155
- .from(contents)
156
- .leftJoin(publishedContents, eq(publishedContents.id, contents.id));
157
-
158
- return rows.map((row) => ({
159
- id: row.id,
160
- spaceId: row.spaceId,
161
- typeId: row.typeId,
162
- draftFields: row.draftFields as Record<string, unknown>,
163
- publishedFields: (row.publishedFields as Record<string, unknown> | null) ?? null,
164
- }));
165
- }
166
- }
@@ -1,181 +0,0 @@
1
- import { type Loose, ManabloxError } from '@manablox/core';
2
- import { and, desc, eq, inArray, sql } from 'drizzle-orm';
3
- import type { Database } from '../client.js';
4
- import { type Paginated, paginate } from '../pagination.js';
5
- import type { Pagination } from '../query.js';
6
- import { type AssetRow, type AssetVariantRow, assets, assetVariants } from '../schema/index.js';
7
-
8
- export interface AssetWriteData {
9
- id?: string | undefined;
10
- spaceId: string;
11
- driver: string;
12
- key: string;
13
- filename: string;
14
- name: string;
15
- mimeType: string;
16
- size: number;
17
- width?: number | null | undefined;
18
- height?: number | null | undefined;
19
- duration?: number | null | undefined;
20
- checksum?: string | null | undefined;
21
- alt?: string | null | undefined;
22
- title?: string | null | undefined;
23
- meta?: Record<string, unknown> | undefined;
24
- actorId?: string | null | undefined;
25
- }
26
-
27
- export interface AssetFilter {
28
- spaceId: string;
29
- /** Prefix match on the mime type, e.g. `image/`. */
30
- mimeType?: string | undefined;
31
- search?: string | undefined;
32
- }
33
-
34
- export class AssetRepository {
35
- constructor(private readonly db: Database) {}
36
-
37
- async findById(id: string): Promise<AssetRow | null> {
38
- const rows = await this.db.select().from(assets).where(eq(assets.id, id)).limit(1);
39
- return rows[0] ?? null;
40
- }
41
-
42
- /**
43
- * `spaceId` is not an optimisation. On the public instance an asset id is the only
44
- * thing a caller supplies, and without this predicate any id resolves — including one
45
- * belonging to another tenant sharing the process.
46
- */
47
- async findManyByIds(ids: string[], spaceId?: string | null): Promise<AssetRow[]> {
48
- if (ids.length === 0) return [];
49
- const where = spaceId
50
- ? and(inArray(assets.id, ids), eq(assets.spaceId, spaceId))
51
- : inArray(assets.id, ids);
52
- return this.db.select().from(assets).where(where);
53
- }
54
-
55
- async findByChecksum(spaceId: string, checksum: string): Promise<AssetRow | null> {
56
- const rows = await this.db
57
- .select()
58
- .from(assets)
59
- .where(and(eq(assets.spaceId, spaceId), eq(assets.checksum, checksum)))
60
- .limit(1);
61
- return rows[0] ?? null;
62
- }
63
-
64
- async list(filter: AssetFilter, pagination: Pagination): Promise<Paginated<AssetRow>> {
65
- const conditions = [eq(assets.spaceId, filter.spaceId)];
66
- if (filter.mimeType) conditions.push(sql`${assets.mimeType} like ${`${filter.mimeType}%`}`);
67
- if (filter.search) {
68
- conditions.push(
69
- sql`(${assets.name} ilike ${`%${filter.search}%`} or ${assets.filename} ilike ${`%${filter.search}%`})`,
70
- );
71
- }
72
-
73
- return paginate(this.db, assets, {
74
- where: and(...conditions),
75
- orderBy: desc(assets.createdAt),
76
- pagination,
77
- });
78
- }
79
-
80
- async create(data: AssetWriteData): Promise<AssetRow> {
81
- const [row] = await this.db
82
- .insert(assets)
83
- .values({
84
- ...(data.id ? { id: data.id } : {}),
85
- spaceId: data.spaceId,
86
- driver: data.driver,
87
- key: data.key,
88
- filename: data.filename,
89
- name: data.name,
90
- mimeType: data.mimeType,
91
- size: data.size,
92
- width: data.width ?? null,
93
- height: data.height ?? null,
94
- duration: data.duration ?? null,
95
- checksum: data.checksum ?? null,
96
- alt: data.alt ?? null,
97
- title: data.title ?? null,
98
- meta: data.meta ?? {},
99
- createdBy: data.actorId ?? null,
100
- })
101
- .returning();
102
- if (!row) throw new ManabloxError('asset.create.failed');
103
- return row;
104
- }
105
-
106
- async update(
107
- id: string,
108
- data: Loose<Pick<AssetWriteData, 'name' | 'alt' | 'title' | 'meta'>>,
109
- ): Promise<AssetRow> {
110
- const [row] = await this.db
111
- .update(assets)
112
- .set({ ...data, updatedAt: new Date() })
113
- .where(eq(assets.id, id))
114
- .returning();
115
- if (!row) throw ManabloxError.notFound('asset.notFound', { id });
116
- return row;
117
- }
118
-
119
- async delete(id: string): Promise<AssetRow | null> {
120
- const [row] = await this.db.delete(assets).where(eq(assets.id, id)).returning();
121
- return row ?? null;
122
- }
123
-
124
- async variants(assetIds: string[]): Promise<AssetVariantRow[]> {
125
- if (assetIds.length === 0) return [];
126
- return this.db.select().from(assetVariants).where(inArray(assetVariants.assetId, assetIds));
127
- }
128
-
129
- async findVariant(
130
- assetId: string,
131
- preset: string,
132
- format: string,
133
- ): Promise<AssetVariantRow | null> {
134
- const rows = await this.db
135
- .select()
136
- .from(assetVariants)
137
- .where(
138
- and(
139
- eq(assetVariants.assetId, assetId),
140
- eq(assetVariants.preset, preset),
141
- eq(assetVariants.format, format),
142
- ),
143
- )
144
- .limit(1);
145
- return rows[0] ?? null;
146
- }
147
-
148
- /** Drops every variant row; the caller removes the files. */
149
- async deleteVariants(assetId: string): Promise<void> {
150
- await this.db.delete(assetVariants).where(eq(assetVariants.assetId, assetId));
151
- }
152
-
153
- async addVariant(data: {
154
- assetId: string;
155
- preset: string;
156
- format: string;
157
- key: string;
158
- width?: number | null;
159
- height?: number | null;
160
- size: number;
161
- }): Promise<AssetVariantRow> {
162
- const [row] = await this.db
163
- .insert(assetVariants)
164
- .values({
165
- assetId: data.assetId,
166
- preset: data.preset,
167
- format: data.format,
168
- key: data.key,
169
- width: data.width ?? null,
170
- height: data.height ?? null,
171
- size: data.size,
172
- })
173
- .onConflictDoUpdate({
174
- target: [assetVariants.assetId, assetVariants.preset, assetVariants.format],
175
- set: { key: data.key, size: data.size },
176
- })
177
- .returning();
178
- if (!row) throw new ManabloxError('assetVariant.create.failed');
179
- return row;
180
- }
181
- }
@@ -1,116 +0,0 @@
1
- import {
2
- type ContentTypeDefinition,
3
- type ContentTypeInput,
4
- defineContentType,
5
- ManabloxError,
6
- } from '@manablox/core';
7
- import { eq } from 'drizzle-orm';
8
- import type { Database } from '../client.js';
9
- import { type ContentTypeRow, contentTypes } from '../schema/index.js';
10
-
11
- /**
12
- * Persistence for *runtime-defined* content types only. Code-defined types come from
13
- * `manablox.config.ts` and are never written here — the registry merges both into one
14
- * shape, and `source` tells the admin which are read-only.
15
- */
16
- export class ContentTypeRepository {
17
- constructor(private readonly db: Database) {}
18
-
19
- async all(): Promise<ContentTypeDefinition[]> {
20
- const rows = await this.db.select().from(contentTypes);
21
- return rows.map(toDefinition);
22
- }
23
-
24
- async findById(id: string): Promise<ContentTypeDefinition | null> {
25
- const rows = await this.db.select().from(contentTypes).where(eq(contentTypes.id, id)).limit(1);
26
- return rows[0] ? toDefinition(rows[0]) : null;
27
- }
28
-
29
- async create(input: ContentTypeInput): Promise<ContentTypeDefinition> {
30
- // Normalise through the same helper the config path uses, so a runtime type and a
31
- // code type are byte-for-byte the same shape.
32
- const definition = defineContentType(input);
33
-
34
- const [row] = await this.db
35
- .insert(contentTypes)
36
- .values({
37
- id: definition.id,
38
- spaceId: definition.spaceId,
39
- name: definition.name,
40
- label: definition.label,
41
- description: definition.description ?? null,
42
- icon: definition.icon ?? null,
43
- kind: definition.kind,
44
- hasSlug: definition.hasSlug,
45
- isPublishable: definition.isPublishable,
46
- isVisibleInTree: definition.isVisibleInTree,
47
- canBeVisibleInMenu: definition.canBeVisibleInMenu,
48
- fields: definition.fields,
49
- })
50
- .returning();
51
-
52
- if (!row) throw new ManabloxError('contentType.create.failed');
53
- return toDefinition(row);
54
- }
55
-
56
- async update(id: string, input: ContentTypeInput): Promise<ContentTypeDefinition> {
57
- const existing = await this.findById(id);
58
- if (!existing) throw ManabloxError.notFound('contentType.notFound', { id });
59
-
60
- const definition = defineContentType({ ...input, id });
61
-
62
- // Field machine names are part of the public GraphQL schema. Renaming one would
63
- // silently break every consumer query, so a rename is expressed as remove + add.
64
- for (const field of definition.fields) {
65
- const previous = existing.fields.find((candidate) => candidate.id === field.id);
66
- if (previous && previous.name !== field.name) {
67
- throw ManabloxError.badRequest('contentType.field.name.immutable', {
68
- from: previous.name,
69
- to: field.name,
70
- });
71
- }
72
- }
73
-
74
- const [row] = await this.db
75
- .update(contentTypes)
76
- .set({
77
- name: definition.name,
78
- label: definition.label,
79
- description: definition.description ?? null,
80
- icon: definition.icon ?? null,
81
- hasSlug: definition.hasSlug,
82
- isPublishable: definition.isPublishable,
83
- isVisibleInTree: definition.isVisibleInTree,
84
- canBeVisibleInMenu: definition.canBeVisibleInMenu,
85
- fields: definition.fields,
86
- updatedAt: new Date(),
87
- })
88
- .where(eq(contentTypes.id, id))
89
- .returning();
90
-
91
- if (!row) throw ManabloxError.notFound('contentType.notFound', { id });
92
- return toDefinition(row);
93
- }
94
-
95
- async delete(id: string): Promise<void> {
96
- await this.db.delete(contentTypes).where(eq(contentTypes.id, id));
97
- }
98
- }
99
-
100
- function toDefinition(row: ContentTypeRow): ContentTypeDefinition {
101
- return {
102
- id: row.id,
103
- name: row.name,
104
- label: row.label,
105
- ...(row.description !== null ? { description: row.description } : {}),
106
- ...(row.icon !== null ? { icon: row.icon } : {}),
107
- kind: row.kind,
108
- spaceId: row.spaceId,
109
- hasSlug: row.hasSlug,
110
- isPublishable: row.isPublishable,
111
- isVisibleInTree: row.isVisibleInTree,
112
- canBeVisibleInMenu: row.canBeVisibleInMenu,
113
- fields: row.fields,
114
- source: 'runtime',
115
- };
116
- }