@manablox/db 0.1.0 → 0.3.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 (44) hide show
  1. package/README.md +21 -0
  2. package/dist/index-Cyf_N5K3.d.ts +658 -0
  3. package/dist/index-rZ24t-Ln.d.ts +4338 -0
  4. package/dist/index.d.ts +123 -0
  5. package/dist/index.js +60 -0
  6. package/dist/repositories-DYjzuuF6.js +1533 -0
  7. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  8. package/dist/schema-Bb4p16Yz.js +539 -0
  9. package/dist/schema.d.ts +2 -0
  10. package/dist/schema.js +2 -0
  11. package/dist/testing.d.ts +77 -0
  12. package/dist/testing.js +217 -0
  13. package/migrations/0005_menus.sql +44 -0
  14. package/migrations/0006_roles.sql +13 -0
  15. package/migrations/0007_apikey-permissions.sql +4 -0
  16. package/migrations/0008_workflows.sql +49 -0
  17. package/migrations/meta/0005_snapshot.json +2504 -0
  18. package/migrations/meta/0006_snapshot.json +2605 -0
  19. package/migrations/meta/0007_snapshot.json +2605 -0
  20. package/migrations/meta/0008_snapshot.json +2986 -0
  21. package/migrations/meta/_journal.json +28 -0
  22. package/package.json +22 -9
  23. package/drizzle.config.ts +0 -11
  24. package/src/bootstrap.ts +0 -13
  25. package/src/cli/migrate.ts +0 -24
  26. package/src/client.ts +0 -37
  27. package/src/columns.ts +0 -39
  28. package/src/index.ts +0 -13
  29. package/src/query.ts +0 -217
  30. package/src/repositories/asset-usage.ts +0 -166
  31. package/src/repositories/asset.ts +0 -189
  32. package/src/repositories/content-type.ts +0 -116
  33. package/src/repositories/content.ts +0 -758
  34. package/src/repositories/index.ts +0 -28
  35. package/src/repositories/space.ts +0 -78
  36. package/src/repositories/user.ts +0 -134
  37. package/src/schema.ts +0 -513
  38. package/test/asset-usage.test.ts +0 -101
  39. package/test/helpers.ts +0 -153
  40. package/test/publish.test.ts +0 -102
  41. package/test/query.test.ts +0 -140
  42. package/test/tree.test.ts +0 -188
  43. package/tsconfig.json +0 -4
  44. package/vitest.config.ts +0 -12
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
@@ -0,0 +1,539 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { relations, sql } from "drizzle-orm";
3
+ import { boolean, customType, index, integer, jsonb, pgTable, primaryKey, text, timestamp, unique, uniqueIndex, uuid } from "drizzle-orm/pg-core";
4
+ //#region src/columns.ts
5
+ /**
6
+ * `ltree` — the materialised ancestor path of a content node. A subtree move is one
7
+ * `UPDATE ... SET path = :newParent || subpath(path, nlevel(:oldParent))`.
8
+ */
9
+ const ltree = customType({ dataType: () => "ltree" });
10
+ /** `tsvector` — generated from `title` + `search_text`, never written directly. */
11
+ const tsvector = customType({ dataType: () => "tsvector" });
12
+ /**
13
+ * ltree labels accept only `[A-Za-z0-9_]`, so UUID hyphens are swapped for underscores.
14
+ * The transform is total and reversible.
15
+ */
16
+ const idToLabel = (id) => id.replaceAll("-", "_");
17
+ const labelToId = (label) => {
18
+ const hex = label.replaceAll("_", "");
19
+ return [
20
+ hex.slice(0, 8),
21
+ hex.slice(8, 12),
22
+ hex.slice(12, 16),
23
+ hex.slice(16, 20),
24
+ hex.slice(20, 32)
25
+ ].join("-");
26
+ };
27
+ /** Builds the path of a node from its ancestors' ids (root first) plus its own. */
28
+ const buildPath = (ancestorIds, selfId) => [...ancestorIds, selfId].map(idToLabel).join(".");
29
+ /** Splits a stored path back into ids, root first, self last. */
30
+ const parsePath = (path) => path.split(".").filter(Boolean).map(labelToId);
31
+ const pathDepth = (path) => path ? path.split(".").length : 0;
32
+ //#endregion
33
+ //#region src/schema/spaces.ts
34
+ const spaces = pgTable("spaces", {
35
+ id: uuid().primaryKey().defaultRandom(),
36
+ name: text().notNull(),
37
+ machineName: text().notNull(),
38
+ description: text(),
39
+ /** Public origin of the space's frontend — used by the visual editor iframe. */
40
+ url: text().notNull(),
41
+ defaultLocale: text().notNull().default("en"),
42
+ locales: jsonb().$type().notNull().default(sql`'["en"]'::jsonb`),
43
+ settings: jsonb().$type().notNull().default(sql`'{}'::jsonb`),
44
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
45
+ updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow()
46
+ }, (table) => [uniqueIndex("spaces_machine_name_key").on(table.machineName)]);
47
+ //#endregion
48
+ //#region src/schema/content.ts
49
+ /** Shared column set for `contents` and its published projection. */
50
+ const contentColumns = {
51
+ id: uuid().primaryKey().defaultRandom(),
52
+ spaceId: uuid().notNull().references(() => spaces.id, { onDelete: "cascade" }),
53
+ typeId: uuid().notNull(),
54
+ locale: text().notNull(),
55
+ /** Shared by all translations of one logical document. */
56
+ localizationId: uuid().notNull(),
57
+ parentId: uuid(),
58
+ title: text().notNull(),
59
+ slug: text().notNull(),
60
+ /** Materialised ancestor path, root first, ending in this node's own id. */
61
+ path: ltree().notNull(),
62
+ /** Routable URL path. NULL when the content type has no slug — such a node is a
63
+ * structural container, not a page, so it must not occupy a permalink of its own. */
64
+ permalink: text(),
65
+ /**
66
+ * Accumulated permalink prefix: every ancestor segment plus this node's own, with
67
+ * slug-less levels skipped. Descendants derive from this rather than from `permalink`,
68
+ * which is NULL for a slug-less node and would otherwise truncate the chain.
69
+ */
70
+ permalinkPath: text().notNull().default(""),
71
+ /**
72
+ * This node's own contribution to its descendants' permalinks: the slug when the
73
+ * content type has one, NULL when it does not.
74
+ *
75
+ * Denormalised deliberately, so permalink recomputation is a single recursive CTE that
76
+ * needs no knowledge of the type registry.
77
+ */
78
+ permalinkSegment: text(),
79
+ status: text().$type().notNull().default("draft"),
80
+ position: integer().notNull().default(0),
81
+ fields: jsonb().$type().notNull().default(sql`'{}'::jsonb`),
82
+ /** Concatenated contributions from each field type's `search()`. */
83
+ searchText: text().notNull().default(""),
84
+ /** Optimistic-lock counter; a write with a stale version is rejected. */
85
+ version: integer().notNull().default(1),
86
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
87
+ updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
88
+ createdBy: uuid(),
89
+ updatedBy: uuid(),
90
+ publishedAt: timestamp({ withTimezone: true })
91
+ };
92
+ const contents = pgTable("contents", {
93
+ ...contentColumns,
94
+ search: tsvector().generatedAlwaysAs(sql`to_tsvector('simple', coalesce(title, '') || ' ' || coalesce(search_text, ''))`)
95
+ }, (table) => [
96
+ index("contents_path_gist_idx").using("gist", table.path),
97
+ index("contents_parent_idx").on(table.parentId),
98
+ index("contents_space_type_idx").on(table.spaceId, table.typeId),
99
+ index("contents_localization_idx").on(table.localizationId),
100
+ index("contents_status_idx").on(table.spaceId, table.status),
101
+ index("contents_fields_gin_idx").using("gin", sql`${table.fields} jsonb_path_ops`),
102
+ index("contents_search_gin_idx").using("gin", table.search),
103
+ unique("contents_sibling_slug_key").on(table.spaceId, table.parentId, table.locale, table.slug).nullsNotDistinct(),
104
+ uniqueIndex("contents_permalink_key").on(table.spaceId, table.locale, table.permalink).where(sql`permalink is not null`)
105
+ ]);
106
+ /**
107
+ * The delivery projection. Written transactionally on publish, read by the public
108
+ * GraphQL API. Mirrors `contents` so a row can be copied column-for-column.
109
+ */
110
+ const publishedContents = pgTable("published_contents", {
111
+ ...contentColumns,
112
+ /** Version of `contents` this projection was made from. */
113
+ sourceVersion: integer().notNull().default(1),
114
+ search: tsvector().generatedAlwaysAs(sql`to_tsvector('simple', coalesce(title, '') || ' ' || coalesce(search_text, ''))`)
115
+ }, (table) => [
116
+ index("published_contents_path_gist_idx").using("gist", table.path),
117
+ index("published_contents_parent_idx").on(table.parentId),
118
+ index("published_contents_space_type_idx").on(table.spaceId, table.typeId),
119
+ index("published_contents_fields_gin_idx").using("gin", sql`${table.fields} jsonb_path_ops`),
120
+ index("published_contents_search_gin_idx").using("gin", table.search),
121
+ uniqueIndex("published_contents_permalink_key").on(table.spaceId, table.locale, table.permalink).where(sql`permalink is not null`)
122
+ ]);
123
+ /** Full snapshot per save, so any edit can be rolled back. */
124
+ const contentVersions = pgTable("content_versions", {
125
+ id: uuid().primaryKey().defaultRandom(),
126
+ contentId: uuid().notNull(),
127
+ version: integer().notNull(),
128
+ label: text(),
129
+ snapshot: jsonb().$type().notNull(),
130
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
131
+ createdBy: uuid()
132
+ }, (table) => [uniqueIndex("content_versions_content_version_key").on(table.contentId, table.version), index("content_versions_content_idx").on(table.contentId, table.createdAt)]);
133
+ //#endregion
134
+ //#region src/schema/assets.ts
135
+ const assets = pgTable("assets", {
136
+ id: uuid().primaryKey().defaultRandom(),
137
+ spaceId: uuid().notNull().references(() => spaces.id, { onDelete: "cascade" }),
138
+ driver: text().notNull().default("local"),
139
+ /** Storage-relative key, e.g. `<space>/2026/09/photo.jpg`. */
140
+ key: text().notNull(),
141
+ filename: text().notNull(),
142
+ name: text().notNull(),
143
+ mimeType: text().notNull(),
144
+ size: integer().notNull(),
145
+ width: integer(),
146
+ height: integer(),
147
+ /** Seconds, for audio/video. */
148
+ duration: integer(),
149
+ /** SHA-256 of the bytes, for dedupe. */
150
+ checksum: text(),
151
+ alt: text(),
152
+ title: text(),
153
+ meta: jsonb().$type().notNull().default(sql`'{}'::jsonb`),
154
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
155
+ updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
156
+ createdBy: uuid()
157
+ }, (table) => [
158
+ uniqueIndex("assets_driver_key_key").on(table.driver, table.key),
159
+ index("assets_space_idx").on(table.spaceId, table.createdAt),
160
+ index("assets_checksum_idx").on(table.spaceId, table.checksum),
161
+ index("assets_mime_idx").on(table.spaceId, table.mimeType)
162
+ ]);
163
+ const assetVariants = pgTable("asset_variants", {
164
+ id: uuid().primaryKey().defaultRandom(),
165
+ assetId: uuid().notNull().references(() => assets.id, { onDelete: "cascade" }),
166
+ preset: text().notNull(),
167
+ format: text().notNull(),
168
+ key: text().notNull(),
169
+ width: integer(),
170
+ height: integer(),
171
+ size: integer().notNull(),
172
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow()
173
+ }, (table) => [uniqueIndex("asset_variants_asset_preset_format_key").on(table.assetId, table.preset, table.format)]);
174
+ /**
175
+ * Which documents reference which assets, and whether any of them is published.
176
+ *
177
+ * Assets have no draft/published distinction of their own, so before this table an
178
+ * asset id resolved on the public API whether or not anything published pointed at it —
179
+ * including a file uploaded for a draft that never shipped. Maintained from the same
180
+ * publish/unpublish/delete hooks that purge the cache, out of the references each field
181
+ * type already declares.
182
+ */
183
+ const assetUsages = pgTable("asset_usages", {
184
+ assetId: uuid().notNull().references(() => assets.id, { onDelete: "cascade" }),
185
+ contentId: uuid().notNull().references(() => contents.id, { onDelete: "cascade" }),
186
+ spaceId: uuid().notNull().references(() => spaces.id, { onDelete: "cascade" }),
187
+ /** True when the *published* projection of `contentId` references the asset. */
188
+ published: boolean().notNull().default(false),
189
+ updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow()
190
+ }, (table) => [
191
+ primaryKey({ columns: [table.assetId, table.contentId] }),
192
+ index("asset_usages_published_idx").on(table.assetId, table.published),
193
+ index("asset_usages_content_idx").on(table.contentId)
194
+ ]);
195
+ //#endregion
196
+ //#region src/schema/auth.ts
197
+ const users = pgTable("users", {
198
+ id: uuid().primaryKey().defaultRandom(),
199
+ name: text().notNull().default(""),
200
+ email: text().notNull(),
201
+ emailVerified: boolean().notNull().default(false),
202
+ image: text(),
203
+ /** Instance-wide role. Space-scoped roles live on `memberships`. */
204
+ role: text().notNull().default("editor"),
205
+ banned: boolean().notNull().default(false),
206
+ banReason: text(),
207
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
208
+ updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow()
209
+ }, (table) => [uniqueIndex("users_email_key").on(table.email)]);
210
+ /** One row per active session, so multiple devices can be signed in at once. */
211
+ const sessions = pgTable("sessions", {
212
+ id: uuid().primaryKey().defaultRandom(),
213
+ userId: uuid().notNull().references(() => users.id, { onDelete: "cascade" }),
214
+ token: text().notNull(),
215
+ expiresAt: timestamp({ withTimezone: true }).notNull(),
216
+ ipAddress: text(),
217
+ userAgent: text(),
218
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
219
+ updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow()
220
+ }, (table) => [
221
+ uniqueIndex("sessions_token_key").on(table.token),
222
+ index("sessions_user_idx").on(table.userId),
223
+ index("sessions_expires_idx").on(table.expiresAt)
224
+ ]);
225
+ const accounts = pgTable("accounts", {
226
+ id: uuid().primaryKey().defaultRandom(),
227
+ userId: uuid().notNull().references(() => users.id, { onDelete: "cascade" }),
228
+ accountId: text().notNull(),
229
+ providerId: text().notNull(),
230
+ /** Required by better-auth 1.7 for OIDC issuer disambiguation. */
231
+ issuer: text().notNull().default(""),
232
+ accessToken: text(),
233
+ refreshToken: text(),
234
+ accessTokenExpiresAt: timestamp({ withTimezone: true }),
235
+ refreshTokenExpiresAt: timestamp({ withTimezone: true }),
236
+ scope: text(),
237
+ idToken: text(),
238
+ password: text(),
239
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
240
+ updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow()
241
+ }, (table) => [uniqueIndex("accounts_provider_account_key").on(table.providerId, table.accountId)]);
242
+ const verifications = pgTable("verifications", {
243
+ id: uuid().primaryKey().defaultRandom(),
244
+ identifier: text().notNull(),
245
+ value: text().notNull(),
246
+ expiresAt: timestamp({ withTimezone: true }).notNull(),
247
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
248
+ updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow()
249
+ }, (table) => [index("verifications_identifier_idx").on(table.identifier)]);
250
+ const apikeys = pgTable("apikeys", {
251
+ id: uuid().primaryKey().defaultRandom(),
252
+ name: text(),
253
+ start: text(),
254
+ prefix: text(),
255
+ key: text().notNull(),
256
+ userId: uuid().notNull().references(() => users.id, { onDelete: "cascade" }),
257
+ enabled: boolean().notNull().default(true),
258
+ expiresAt: timestamp({ withTimezone: true }),
259
+ lastRequest: timestamp({ withTimezone: true }),
260
+ /**
261
+ * Grants this key is confined to, in the roles' vocabulary; `null` means whatever
262
+ * the owner's role allows. Like `spaceIds`, a restriction only ever narrows.
263
+ */
264
+ permissions: jsonb().$type(),
265
+ /**
266
+ * Space ids this key may act in; `null` means every space the owner belongs to.
267
+ * A restriction only ever narrows the owner's own access, never widens it.
268
+ */
269
+ spaceIds: jsonb().$type(),
270
+ metadata: text(),
271
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
272
+ updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow()
273
+ }, (table) => [index("apikeys_user_idx").on(table.userId)]);
274
+ /** Space-scoped role assignment. */
275
+ const memberships = pgTable("memberships", {
276
+ userId: uuid().notNull().references(() => users.id, { onDelete: "cascade" }),
277
+ spaceId: uuid().notNull().references(() => spaces.id, { onDelete: "cascade" }),
278
+ /** The machine name of a built-in role or of a row in `roles` for the same space. */
279
+ role: text().notNull().default("editor"),
280
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow()
281
+ }, (table) => [primaryKey({ columns: [table.userId, table.spaceId] }), index("memberships_space_idx").on(table.spaceId)]);
282
+ /**
283
+ * A role someone created for a space, beside the built-in five. `permissions` holds the
284
+ * grants: `space:write`, `content:read` for every type, or `content:read:<typeId>` for
285
+ * one. Memberships name it by `machineName`, so a custom role is one row and one name.
286
+ */
287
+ const roles = pgTable("roles", {
288
+ id: uuid().primaryKey().defaultRandom(),
289
+ spaceId: uuid().notNull().references(() => spaces.id, { onDelete: "cascade" }),
290
+ name: text().notNull(),
291
+ machineName: text().notNull(),
292
+ description: text(),
293
+ permissions: jsonb().$type().notNull().default(sql`'[]'::jsonb`),
294
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
295
+ updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow()
296
+ }, (table) => [uniqueIndex("roles_space_machine_name_key").on(table.spaceId, table.machineName)]);
297
+ //#endregion
298
+ //#region src/schema/content-types.ts
299
+ /** Runtime-defined content types only; code-defined ones live in the registry. */
300
+ const contentTypes = pgTable("content_types", {
301
+ id: uuid().primaryKey().defaultRandom(),
302
+ spaceId: uuid().references(() => spaces.id, { onDelete: "cascade" }),
303
+ name: text().notNull(),
304
+ label: text().notNull(),
305
+ description: text(),
306
+ icon: text(),
307
+ kind: text().$type().notNull().default("content"),
308
+ hasSlug: boolean().notNull().default(true),
309
+ isPublishable: boolean().notNull().default(true),
310
+ isVisibleInTree: boolean().notNull().default(true),
311
+ canBeVisibleInMenu: boolean().notNull().default(true),
312
+ fields: jsonb().$type().notNull().default(sql`'[]'::jsonb`),
313
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
314
+ updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow()
315
+ }, (table) => [unique("content_types_space_name_key").on(table.spaceId, table.name).nullsNotDistinct(), index("content_types_space_idx").on(table.spaceId)]);
316
+ //#endregion
317
+ //#region src/schema/menus.ts
318
+ /**
319
+ * A named navigation a site renders: "main", "footer", "legal". A document may sit in
320
+ * any number of them, which is why membership is a row here and not a flag on the
321
+ * document.
322
+ */
323
+ const menus = pgTable("menus", {
324
+ id: uuid().primaryKey().defaultRandom(),
325
+ spaceId: uuid().notNull().references(() => spaces.id, { onDelete: "cascade" }),
326
+ name: text().notNull(),
327
+ /** What the delivery API is asked for: `menu(name: "main")`. */
328
+ machineName: text().notNull(),
329
+ description: text(),
330
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
331
+ updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow()
332
+ }, (table) => [unique("menus_space_machine_name_key").on(table.spaceId, table.machineName)]);
333
+ /**
334
+ * One entry of a menu, nested to any depth.
335
+ *
336
+ * A content entry points at the document's `localizationId` rather than at one row, so
337
+ * the same menu serves every locale: delivery resolves the translation for the locale
338
+ * it is asked for. A link entry has a `url` instead. Either may carry a `label`; for a
339
+ * content entry it overrides the document's title.
340
+ */
341
+ const menuItems = pgTable("menu_items", {
342
+ id: uuid().primaryKey().defaultRandom(),
343
+ menuId: uuid().notNull().references(() => menus.id, { onDelete: "cascade" }),
344
+ /** Cascades, so removing an entry takes its sub-entries with it. */
345
+ parentId: uuid().references(() => menuItems.id, { onDelete: "cascade" }),
346
+ position: integer().notNull().default(0),
347
+ localizationId: uuid(),
348
+ label: text(),
349
+ url: text()
350
+ }, (table) => [index("menu_items_menu_idx").on(table.menuId, table.parentId, table.position), index("menu_items_localization_idx").on(table.localizationId)]);
351
+ //#endregion
352
+ //#region src/schema/relations.ts
353
+ const spacesRelations = relations(spaces, ({ many }) => ({
354
+ contents: many(contents),
355
+ contentTypes: many(contentTypes),
356
+ assets: many(assets),
357
+ memberships: many(memberships),
358
+ menus: many(menus),
359
+ roles: many(roles)
360
+ }));
361
+ const contentsRelations = relations(contents, ({ one, many }) => ({
362
+ space: one(spaces, {
363
+ fields: [contents.spaceId],
364
+ references: [spaces.id]
365
+ }),
366
+ parent: one(contents, {
367
+ fields: [contents.parentId],
368
+ references: [contents.id],
369
+ relationName: "tree"
370
+ }),
371
+ children: many(contents, { relationName: "tree" }),
372
+ versions: many(contentVersions)
373
+ }));
374
+ const assetsRelations = relations(assets, ({ one, many }) => ({
375
+ space: one(spaces, {
376
+ fields: [assets.spaceId],
377
+ references: [spaces.id]
378
+ }),
379
+ variants: many(assetVariants)
380
+ }));
381
+ const assetVariantsRelations = relations(assetVariants, ({ one }) => ({ asset: one(assets, {
382
+ fields: [assetVariants.assetId],
383
+ references: [assets.id]
384
+ }) }));
385
+ const assetUsagesRelations = relations(assetUsages, ({ one }) => ({
386
+ asset: one(assets, {
387
+ fields: [assetUsages.assetId],
388
+ references: [assets.id]
389
+ }),
390
+ content: one(contents, {
391
+ fields: [assetUsages.contentId],
392
+ references: [contents.id]
393
+ })
394
+ }));
395
+ const usersRelations = relations(users, ({ many }) => ({
396
+ sessions: many(sessions),
397
+ memberships: many(memberships)
398
+ }));
399
+ const membershipsRelations = relations(memberships, ({ one }) => ({
400
+ user: one(users, {
401
+ fields: [memberships.userId],
402
+ references: [users.id]
403
+ }),
404
+ space: one(spaces, {
405
+ fields: [memberships.spaceId],
406
+ references: [spaces.id]
407
+ })
408
+ }));
409
+ const menusRelations = relations(menus, ({ one, many }) => ({
410
+ space: one(spaces, {
411
+ fields: [menus.spaceId],
412
+ references: [spaces.id]
413
+ }),
414
+ items: many(menuItems)
415
+ }));
416
+ const menuItemsRelations = relations(menuItems, ({ one }) => ({ menu: one(menus, {
417
+ fields: [menuItems.menuId],
418
+ references: [menus.id]
419
+ }) }));
420
+ const rolesRelations = relations(roles, ({ one }) => ({ space: one(spaces, {
421
+ fields: [roles.spaceId],
422
+ references: [spaces.id]
423
+ }) }));
424
+ //#endregion
425
+ //#region src/schema/webhooks.ts
426
+ const webhooks = pgTable("webhooks", {
427
+ id: uuid().primaryKey().defaultRandom(),
428
+ spaceId: uuid().notNull().references(() => spaces.id, { onDelete: "cascade" }),
429
+ name: text().notNull(),
430
+ url: text().notNull(),
431
+ secret: text(),
432
+ events: jsonb().$type().notNull().default(sql`'[]'::jsonb`),
433
+ enabled: boolean().notNull().default(true),
434
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow()
435
+ }, (table) => [index("webhooks_space_idx").on(table.spaceId)]);
436
+ const webhookDeliveries = pgTable("webhook_deliveries", {
437
+ id: uuid().primaryKey().defaultRandom(),
438
+ webhookId: uuid().notNull().references(() => webhooks.id, { onDelete: "cascade" }),
439
+ event: text().notNull(),
440
+ payload: jsonb().$type().notNull(),
441
+ status: integer(),
442
+ error: text(),
443
+ attempt: integer().notNull().default(1),
444
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow()
445
+ }, (table) => [index("webhook_deliveries_webhook_idx").on(table.webhookId, table.createdAt)]);
446
+ //#endregion
447
+ //#region src/schema/workflows.ts
448
+ /**
449
+ * A workflow: something that happens when content changes, or on a schedule. The
450
+ * trigger and the steps are documents rather than rows — a workflow is edited and saved
451
+ * as one thing, and a step type added later needs no migration.
452
+ */
453
+ const workflows = pgTable("workflows", {
454
+ id: uuid().primaryKey().defaultRandom(),
455
+ spaceId: uuid().notNull().references(() => spaces.id, { onDelete: "cascade" }),
456
+ name: text().notNull(),
457
+ description: text(),
458
+ enabled: boolean().notNull().default(false),
459
+ trigger: jsonb().$type().notNull(),
460
+ steps: jsonb().$type().notNull().default(sql`'[]'::jsonb`),
461
+ /**
462
+ * The minute a scheduled workflow was last claimed for, so two processes ticking at
463
+ * once start one run between them rather than one each.
464
+ */
465
+ lastScheduledAt: timestamp({ withTimezone: true }),
466
+ lastRunAt: timestamp({ withTimezone: true }),
467
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
468
+ updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow()
469
+ }, (table) => [index("workflows_space_idx").on(table.spaceId, table.enabled)]);
470
+ /**
471
+ * One execution of a workflow, with everything needed to continue it after a pause: the
472
+ * context the templates read, the index of the next step and the log so far.
473
+ */
474
+ const workflowRuns = pgTable("workflow_runs", {
475
+ id: uuid().primaryKey().defaultRandom(),
476
+ workflowId: uuid().notNull().references(() => workflows.id, { onDelete: "cascade" }),
477
+ spaceId: uuid().notNull().references(() => spaces.id, { onDelete: "cascade" }),
478
+ /** The event name, or `schedule` / `manual`. */
479
+ trigger: text().notNull(),
480
+ status: text().$type().notNull().default("queued"),
481
+ context: jsonb().$type().notNull(),
482
+ /** Where to continue; a paused run resumes here. See `WorkflowCursor`. */
483
+ cursor: jsonb().$type().notNull().default(sql`'[]'::jsonb`),
484
+ log: jsonb().$type().notNull().default(sql`'[]'::jsonb`),
485
+ error: text(),
486
+ /** Set while `waiting`: when the scheduler should pick the run up again. */
487
+ resumeAt: timestamp({ withTimezone: true }),
488
+ startedAt: timestamp({ withTimezone: true }),
489
+ finishedAt: timestamp({ withTimezone: true }),
490
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow()
491
+ }, (table) => [index("workflow_runs_workflow_idx").on(table.workflowId, table.createdAt), index("workflow_runs_resume_idx").on(table.status, table.resumeAt)]);
492
+ /** A browser a user allowed push notifications in; one row per device. */
493
+ const pushSubscriptions = pgTable("push_subscriptions", {
494
+ id: uuid().primaryKey().defaultRandom(),
495
+ userId: uuid().notNull().references(() => users.id, { onDelete: "cascade" }),
496
+ endpoint: text().notNull().unique("push_subscriptions_endpoint_key"),
497
+ keys: jsonb().$type().notNull(),
498
+ userAgent: text(),
499
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
500
+ lastUsedAt: timestamp({ withTimezone: true })
501
+ }, (table) => [index("push_subscriptions_user_idx").on(table.userId)]);
502
+ //#endregion
503
+ //#region src/schema/index.ts
504
+ var schema_exports = /* @__PURE__ */ __exportAll({
505
+ accounts: () => accounts,
506
+ apikeys: () => apikeys,
507
+ assetUsages: () => assetUsages,
508
+ assetUsagesRelations: () => assetUsagesRelations,
509
+ assetVariants: () => assetVariants,
510
+ assetVariantsRelations: () => assetVariantsRelations,
511
+ assets: () => assets,
512
+ assetsRelations: () => assetsRelations,
513
+ contentTypes: () => contentTypes,
514
+ contentVersions: () => contentVersions,
515
+ contents: () => contents,
516
+ contentsRelations: () => contentsRelations,
517
+ memberships: () => memberships,
518
+ membershipsRelations: () => membershipsRelations,
519
+ menuItems: () => menuItems,
520
+ menuItemsRelations: () => menuItemsRelations,
521
+ menus: () => menus,
522
+ menusRelations: () => menusRelations,
523
+ publishedContents: () => publishedContents,
524
+ pushSubscriptions: () => pushSubscriptions,
525
+ roles: () => roles,
526
+ rolesRelations: () => rolesRelations,
527
+ sessions: () => sessions,
528
+ spaces: () => spaces,
529
+ spacesRelations: () => spacesRelations,
530
+ users: () => users,
531
+ usersRelations: () => usersRelations,
532
+ verifications: () => verifications,
533
+ webhookDeliveries: () => webhookDeliveries,
534
+ webhooks: () => webhooks,
535
+ workflowRuns: () => workflowRuns,
536
+ workflows: () => workflows
537
+ });
538
+ //#endregion
539
+ export { contentVersions as A, tsvector as B, roles as C, assetUsages as D, verifications as E, idToLabel as F, labelToId as I, ltree as L, publishedContents as M, spaces as N, assetVariants as O, buildPath as P, parsePath as R, memberships as S, users as T, menuItems as _, webhookDeliveries as a, accounts as b, assetVariantsRelations as c, membershipsRelations as d, menuItemsRelations as f, usersRelations as g, spacesRelations as h, workflows as i, contents as j, assets as k, assetsRelations as l, rolesRelations as m, pushSubscriptions as n, webhooks as o, menusRelations as p, workflowRuns as r, assetUsagesRelations as s, schema_exports as t, contentsRelations as u, menus as v, sessions as w, apikeys as x, contentTypes as y, pathDepth as z };
@@ -0,0 +1,2 @@
1
+ import { A as menusRelations, B as accounts, C as spaces, D as contentsRelations, E as assetsRelations, F as menus, G as users, H as memberships, I as contentTypes, J as assetVariants, K as verifications, L as contentVersions, M as spacesRelations, N as usersRelations, O as membershipsRelations, P as menuItems, R as contents, S as webhooks, T as assetVariantsRelations, U as roles, V as apikeys, W as sessions, Y as assets, a as ContentRow, b as workflows, c as MembershipRow, d as PushSubscriptionRow, f as RoleRow, g as WorkflowRunRow, h as WorkflowRow, i as ContentInsert, j as rolesRelations, k as menuItemsRelations, l as MenuItemRow, m as UserRow, n as AssetUsageRow, o as ContentTypeRow, p as SpaceRow, q as assetUsages, r as AssetVariantRow, s as ContentVersionRow, t as AssetRow, u as MenuRow, v as pushSubscriptions, w as assetUsagesRelations, x as webhookDeliveries, y as workflowRuns, z as publishedContents } from "./index-rZ24t-Ln.js";
2
+ export { AssetRow, AssetUsageRow, AssetVariantRow, ContentInsert, ContentRow, ContentTypeRow, ContentVersionRow, MembershipRow, MenuItemRow, MenuRow, PushSubscriptionRow, RoleRow, SpaceRow, UserRow, WorkflowRow, WorkflowRunRow, accounts, apikeys, assetUsages, assetUsagesRelations, assetVariants, assetVariantsRelations, assets, assetsRelations, contentTypes, contentVersions, contents, contentsRelations, memberships, membershipsRelations, menuItems, menuItemsRelations, menus, menusRelations, publishedContents, pushSubscriptions, roles, rolesRelations, sessions, spaces, spacesRelations, users, usersRelations, verifications, webhookDeliveries, webhooks, workflowRuns, workflows };
package/dist/schema.js ADDED
@@ -0,0 +1,2 @@
1
+ import { A as contentVersions, C as roles, D as assetUsages, E as verifications, M as publishedContents, N as spaces, O as assetVariants, S as memberships, T as users, _ as menuItems, a as webhookDeliveries, b as accounts, c as assetVariantsRelations, d as membershipsRelations, f as menuItemsRelations, g as usersRelations, h as spacesRelations, i as workflows, j as contents, k as assets, l as assetsRelations, m as rolesRelations, n as pushSubscriptions, o as webhooks, p as menusRelations, r as workflowRuns, s as assetUsagesRelations, u as contentsRelations, v as menus, w as sessions, x as apikeys, y as contentTypes } from "./schema-Bb4p16Yz.js";
2
+ export { accounts, apikeys, assetUsages, assetUsagesRelations, assetVariants, assetVariantsRelations, assets, assetsRelations, contentTypes, contentVersions, contents, contentsRelations, memberships, membershipsRelations, menuItems, menuItemsRelations, menus, menusRelations, publishedContents, pushSubscriptions, roles, rolesRelations, sessions, spaces, spacesRelations, users, usersRelations, verifications, webhookDeliveries, webhooks, workflowRuns, workflows };
@@ -0,0 +1,77 @@
1
+ import { U as DatabaseHandle, t as Repositories } from "./index-Cyf_N5K3.js";
2
+ import { ContentTypeDefinition, ContentTypeRegistry } from "@manablox/core";
3
+ //#region src/testing-fixtures.d.ts
4
+ export declare const stringField: import("@manablox/core").FieldTypeDefinition<Record<string, unknown>, string>;
5
+ export declare const numberField: import("@manablox/core").FieldTypeDefinition<Record<string, unknown>, number>;
6
+ export interface RepositoryTestContext {
7
+ handle: DatabaseHandle;
8
+ /** Every statement sent to Postgres, so a test can assert what a call cost. */
9
+ queries: string[];
10
+ repos: Repositories;
11
+ registry: ContentTypeRegistry;
12
+ types: {
13
+ page: ContentTypeDefinition;
14
+ folder: ContentTypeDefinition;
15
+ };
16
+ spaceId: string;
17
+ close: () => Promise<void>;
18
+ }
19
+ /** Spins up an isolated database per suite, migrated from the real migration files. */
20
+ export declare function createRepositoryContext(name: string): Promise<RepositoryTestContext>;
21
+ /** Convenience creator so tests read as tree shapes rather than field soup. */
22
+ export declare function makeNode(ctx: RepositoryTestContext, options: {
23
+ title: string;
24
+ slug: string;
25
+ parentId?: string | null;
26
+ type?: 'page' | 'folder';
27
+ fields?: Record<string, unknown>;
28
+ }): Promise<{
29
+ createdAt: Date;
30
+ createdBy: string | null;
31
+ fields: Record<string, unknown>;
32
+ id: string;
33
+ locale: string;
34
+ localizationId: string;
35
+ parentId: string | null;
36
+ path: string;
37
+ permalink: string | null;
38
+ permalinkPath: string;
39
+ permalinkSegment: string | null;
40
+ position: number;
41
+ publishedAt: Date | null;
42
+ search: string | null;
43
+ searchText: string;
44
+ slug: string;
45
+ spaceId: string;
46
+ status: import("@manablox/core").ContentStatus;
47
+ title: string;
48
+ typeId: string;
49
+ updatedAt: Date;
50
+ updatedBy: string | null;
51
+ version: number;
52
+ }>;
53
+ //#endregion
54
+ //#region src/testing.d.ts
55
+ /**
56
+ * Only ever connected to in order to `create database`: every test file works in its own
57
+ * fresh database and drops it again, so this points at a Postgres *server*, not at data
58
+ * the tests touch. That is why falling back to `DATABASE_URL` is safe — it means the
59
+ * suite runs wherever the app itself runs (a container, CI, the host) without a second
60
+ * variable that has to be kept in step. `TEST_DATABASE_URL` still overrides it.
61
+ */
62
+ export declare const TEST_ADMIN_URL: string;
63
+ export declare function withDatabase(url: string, name: string): string;
64
+ export interface TestDatabase {
65
+ name: string;
66
+ url: string;
67
+ /** Drops the database. Safe to call once every connection to it is closed. */
68
+ drop: () => Promise<void>;
69
+ }
70
+ /**
71
+ * A fresh, migrated database for one test suite, cloned from the template.
72
+ *
73
+ * `prefix` names the suite in `pg_database`, which is what you grep for when a run was
74
+ * interrupted and left a database behind.
75
+ */
76
+ export declare function createTestDatabase(prefix: string): Promise<TestDatabase>;
77
+ //#endregion