@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.
- package/README.md +21 -0
- package/dist/index-Cyf_N5K3.d.ts +658 -0
- package/dist/index-rZ24t-Ln.d.ts +4338 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.js +60 -0
- package/dist/repositories-DYjzuuF6.js +1533 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/schema-Bb4p16Yz.js +539 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +2 -0
- package/dist/testing.d.ts +77 -0
- package/dist/testing.js +217 -0
- package/migrations/0005_menus.sql +44 -0
- package/migrations/0006_roles.sql +13 -0
- package/migrations/0007_apikey-permissions.sql +4 -0
- package/migrations/0008_workflows.sql +49 -0
- package/migrations/meta/0005_snapshot.json +2504 -0
- package/migrations/meta/0006_snapshot.json +2605 -0
- package/migrations/meta/0007_snapshot.json +2605 -0
- package/migrations/meta/0008_snapshot.json +2986 -0
- package/migrations/meta/_journal.json +28 -0
- package/package.json +22 -9
- package/drizzle.config.ts +0 -11
- package/src/bootstrap.ts +0 -13
- package/src/cli/migrate.ts +0 -24
- package/src/client.ts +0 -37
- package/src/columns.ts +0 -39
- package/src/index.ts +0 -13
- package/src/query.ts +0 -217
- package/src/repositories/asset-usage.ts +0 -166
- package/src/repositories/asset.ts +0 -189
- package/src/repositories/content-type.ts +0 -116
- package/src/repositories/content.ts +0 -758
- package/src/repositories/index.ts +0 -28
- package/src/repositories/space.ts +0 -78
- package/src/repositories/user.ts +0 -134
- package/src/schema.ts +0 -513
- package/test/asset-usage.test.ts +0 -101
- package/test/helpers.ts +0 -153
- package/test/publish.test.ts +0 -102
- package/test/query.test.ts +0 -140
- package/test/tree.test.ts +0 -188
- package/tsconfig.json +0 -4
- 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 };
|
package/dist/schema.d.ts
ADDED
|
@@ -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
|