@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.
- package/dist/index-BLAkQMJT.d.ts +877 -0
- package/dist/index-DrNMGM9N.d.ts +5193 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.js +60 -0
- package/dist/repositories-pz4NeWaF.js +1928 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/schema-Dm3RcBst.js +648 -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/0009_audit-log.sql +40 -0
- package/migrations/0010_notifications-approvals.sql +45 -0
- package/migrations/meta/0009_snapshot.json +3192 -0
- package/migrations/meta/0010_snapshot.json +3574 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +18 -10
- package/drizzle.config.ts +0 -11
- package/src/bootstrap.ts +0 -13
- package/src/cli/create-db.ts +0 -30
- package/src/cli/migrate.ts +0 -17
- package/src/client.ts +0 -44
- package/src/columns.ts +0 -39
- package/src/errors.ts +0 -50
- package/src/index.ts +0 -19
- package/src/migrate.ts +0 -21
- package/src/pagination.ts +0 -52
- package/src/query.ts +0 -213
- package/src/repositories/asset-usage.ts +0 -166
- package/src/repositories/asset.ts +0 -181
- package/src/repositories/content-type.ts +0 -116
- package/src/repositories/content.ts +0 -811
- package/src/repositories/index.ts +0 -40
- package/src/repositories/menu.ts +0 -235
- package/src/repositories/role.ts +0 -85
- package/src/repositories/space.ts +0 -83
- package/src/repositories/user.ts +0 -280
- package/src/repositories/webhook.ts +0 -46
- package/src/repositories/workflow.ts +0 -306
- package/src/schema/assets.ts +0 -108
- package/src/schema/auth.ts +0 -166
- package/src/schema/content-types.ts +0 -31
- package/src/schema/content.ts +0 -133
- package/src/schema/index.ts +0 -38
- package/src/schema/menus.ts +0 -61
- package/src/schema/relations.ts +0 -64
- package/src/schema/spaces.ts +0 -20
- package/src/schema/webhooks.ts +0 -46
- package/src/schema/workflows.ts +0 -92
- package/src/testing-fixtures.ts +0 -139
- package/src/testing.ts +0 -105
- package/test/asset-usage.test.ts +0 -101
- package/test/menu.test.ts +0 -126
- package/test/publish.test.ts +0 -130
- package/test/query.test.ts +0 -170
- package/test/role.test.ts +0 -81
- package/test/tree.test.ts +0 -188
- package/test/user.test.ts +0 -126
- package/test/webhook.test.ts +0 -48
- package/tsconfig.json +0 -4
- package/vitest.config.ts +0 -10
package/src/schema/assets.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { sql } from 'drizzle-orm';
|
|
2
|
-
import {
|
|
3
|
-
boolean,
|
|
4
|
-
index,
|
|
5
|
-
integer,
|
|
6
|
-
jsonb,
|
|
7
|
-
pgTable,
|
|
8
|
-
primaryKey,
|
|
9
|
-
text,
|
|
10
|
-
timestamp,
|
|
11
|
-
uniqueIndex,
|
|
12
|
-
uuid,
|
|
13
|
-
} from 'drizzle-orm/pg-core';
|
|
14
|
-
import { contents } from './content.js';
|
|
15
|
-
import { spaces } from './spaces.js';
|
|
16
|
-
|
|
17
|
-
export const assets = pgTable(
|
|
18
|
-
'assets',
|
|
19
|
-
{
|
|
20
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
21
|
-
spaceId: uuid()
|
|
22
|
-
.notNull()
|
|
23
|
-
.references(() => spaces.id, { onDelete: 'cascade' }),
|
|
24
|
-
driver: text().notNull().default('local'),
|
|
25
|
-
/** Storage-relative key, e.g. `<space>/2026/09/photo.jpg`. */
|
|
26
|
-
key: text().notNull(),
|
|
27
|
-
filename: text().notNull(),
|
|
28
|
-
name: text().notNull(),
|
|
29
|
-
mimeType: text().notNull(),
|
|
30
|
-
size: integer().notNull(),
|
|
31
|
-
width: integer(),
|
|
32
|
-
height: integer(),
|
|
33
|
-
/** Seconds, for audio/video. */
|
|
34
|
-
duration: integer(),
|
|
35
|
-
/** SHA-256 of the bytes, for dedupe. */
|
|
36
|
-
checksum: text(),
|
|
37
|
-
alt: text(),
|
|
38
|
-
title: text(),
|
|
39
|
-
meta: jsonb().$type<Record<string, unknown>>().notNull().default(sql`'{}'::jsonb`),
|
|
40
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
41
|
-
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
42
|
-
createdBy: uuid(),
|
|
43
|
-
},
|
|
44
|
-
(table) => [
|
|
45
|
-
uniqueIndex('assets_driver_key_key').on(table.driver, table.key),
|
|
46
|
-
index('assets_space_idx').on(table.spaceId, table.createdAt),
|
|
47
|
-
index('assets_checksum_idx').on(table.spaceId, table.checksum),
|
|
48
|
-
index('assets_mime_idx').on(table.spaceId, table.mimeType),
|
|
49
|
-
],
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
export const assetVariants = pgTable(
|
|
53
|
-
'asset_variants',
|
|
54
|
-
{
|
|
55
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
56
|
-
assetId: uuid()
|
|
57
|
-
.notNull()
|
|
58
|
-
.references(() => assets.id, { onDelete: 'cascade' }),
|
|
59
|
-
preset: text().notNull(),
|
|
60
|
-
format: text().notNull(),
|
|
61
|
-
key: text().notNull(),
|
|
62
|
-
width: integer(),
|
|
63
|
-
height: integer(),
|
|
64
|
-
size: integer().notNull(),
|
|
65
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
66
|
-
},
|
|
67
|
-
(table) => [
|
|
68
|
-
uniqueIndex('asset_variants_asset_preset_format_key').on(
|
|
69
|
-
table.assetId,
|
|
70
|
-
table.preset,
|
|
71
|
-
table.format,
|
|
72
|
-
),
|
|
73
|
-
],
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Which documents reference which assets, and whether any of them is published.
|
|
78
|
-
*
|
|
79
|
-
* Assets have no draft/published distinction of their own, so before this table an
|
|
80
|
-
* asset id resolved on the public API whether or not anything published pointed at it —
|
|
81
|
-
* including a file uploaded for a draft that never shipped. Maintained from the same
|
|
82
|
-
* publish/unpublish/delete hooks that purge the cache, out of the references each field
|
|
83
|
-
* type already declares.
|
|
84
|
-
*/
|
|
85
|
-
export const assetUsages = pgTable(
|
|
86
|
-
'asset_usages',
|
|
87
|
-
{
|
|
88
|
-
assetId: uuid()
|
|
89
|
-
.notNull()
|
|
90
|
-
.references(() => assets.id, { onDelete: 'cascade' }),
|
|
91
|
-
contentId: uuid()
|
|
92
|
-
.notNull()
|
|
93
|
-
.references(() => contents.id, { onDelete: 'cascade' }),
|
|
94
|
-
spaceId: uuid()
|
|
95
|
-
.notNull()
|
|
96
|
-
.references(() => spaces.id, { onDelete: 'cascade' }),
|
|
97
|
-
/** True when the *published* projection of `contentId` references the asset. */
|
|
98
|
-
published: boolean().notNull().default(false),
|
|
99
|
-
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
100
|
-
},
|
|
101
|
-
(table) => [
|
|
102
|
-
primaryKey({ columns: [table.assetId, table.contentId] }),
|
|
103
|
-
// The public asset check is `asset_id = any($1) and published`; this index serves it
|
|
104
|
-
// without touching the heap.
|
|
105
|
-
index('asset_usages_published_idx').on(table.assetId, table.published),
|
|
106
|
-
index('asset_usages_content_idx').on(table.contentId),
|
|
107
|
-
],
|
|
108
|
-
);
|
package/src/schema/auth.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { sql } from 'drizzle-orm';
|
|
2
|
-
import {
|
|
3
|
-
boolean,
|
|
4
|
-
index,
|
|
5
|
-
jsonb,
|
|
6
|
-
pgTable,
|
|
7
|
-
primaryKey,
|
|
8
|
-
text,
|
|
9
|
-
timestamp,
|
|
10
|
-
uniqueIndex,
|
|
11
|
-
uuid,
|
|
12
|
-
} from 'drizzle-orm/pg-core';
|
|
13
|
-
import { spaces } from './spaces.js';
|
|
14
|
-
|
|
15
|
-
// better-auth owns the user, session, account and verification tables; they are declared
|
|
16
|
-
// here so Drizzle can join and migrate them.
|
|
17
|
-
|
|
18
|
-
export const users = pgTable(
|
|
19
|
-
'users',
|
|
20
|
-
{
|
|
21
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
22
|
-
name: text().notNull().default(''),
|
|
23
|
-
email: text().notNull(),
|
|
24
|
-
emailVerified: boolean().notNull().default(false),
|
|
25
|
-
image: text(),
|
|
26
|
-
/** Instance-wide role. Space-scoped roles live on `memberships`. */
|
|
27
|
-
role: text().notNull().default('editor'),
|
|
28
|
-
banned: boolean().notNull().default(false),
|
|
29
|
-
banReason: text(),
|
|
30
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
31
|
-
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
32
|
-
},
|
|
33
|
-
(table) => [uniqueIndex('users_email_key').on(table.email)],
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
/** One row per active session, so multiple devices can be signed in at once. */
|
|
37
|
-
export const sessions = pgTable(
|
|
38
|
-
'sessions',
|
|
39
|
-
{
|
|
40
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
41
|
-
userId: uuid()
|
|
42
|
-
.notNull()
|
|
43
|
-
.references(() => users.id, { onDelete: 'cascade' }),
|
|
44
|
-
token: text().notNull(),
|
|
45
|
-
expiresAt: timestamp({ withTimezone: true }).notNull(),
|
|
46
|
-
ipAddress: text(),
|
|
47
|
-
userAgent: text(),
|
|
48
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
49
|
-
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
50
|
-
},
|
|
51
|
-
(table) => [
|
|
52
|
-
uniqueIndex('sessions_token_key').on(table.token),
|
|
53
|
-
index('sessions_user_idx').on(table.userId),
|
|
54
|
-
index('sessions_expires_idx').on(table.expiresAt),
|
|
55
|
-
],
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
export const accounts = pgTable(
|
|
59
|
-
'accounts',
|
|
60
|
-
{
|
|
61
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
62
|
-
userId: uuid()
|
|
63
|
-
.notNull()
|
|
64
|
-
.references(() => users.id, { onDelete: 'cascade' }),
|
|
65
|
-
accountId: text().notNull(),
|
|
66
|
-
providerId: text().notNull(),
|
|
67
|
-
/** Required by better-auth 1.7 for OIDC issuer disambiguation. */
|
|
68
|
-
issuer: text().notNull().default(''),
|
|
69
|
-
accessToken: text(),
|
|
70
|
-
refreshToken: text(),
|
|
71
|
-
accessTokenExpiresAt: timestamp({ withTimezone: true }),
|
|
72
|
-
refreshTokenExpiresAt: timestamp({ withTimezone: true }),
|
|
73
|
-
scope: text(),
|
|
74
|
-
idToken: text(),
|
|
75
|
-
password: text(),
|
|
76
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
77
|
-
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
78
|
-
},
|
|
79
|
-
(table) => [uniqueIndex('accounts_provider_account_key').on(table.providerId, table.accountId)],
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
export const verifications = pgTable(
|
|
83
|
-
'verifications',
|
|
84
|
-
{
|
|
85
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
86
|
-
identifier: text().notNull(),
|
|
87
|
-
value: text().notNull(),
|
|
88
|
-
expiresAt: timestamp({ withTimezone: true }).notNull(),
|
|
89
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
90
|
-
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
91
|
-
},
|
|
92
|
-
(table) => [index('verifications_identifier_idx').on(table.identifier)],
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
export const apikeys = pgTable(
|
|
96
|
-
'apikeys',
|
|
97
|
-
{
|
|
98
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
99
|
-
name: text(),
|
|
100
|
-
start: text(),
|
|
101
|
-
prefix: text(),
|
|
102
|
-
key: text().notNull(),
|
|
103
|
-
userId: uuid()
|
|
104
|
-
.notNull()
|
|
105
|
-
.references(() => users.id, { onDelete: 'cascade' }),
|
|
106
|
-
enabled: boolean().notNull().default(true),
|
|
107
|
-
expiresAt: timestamp({ withTimezone: true }),
|
|
108
|
-
lastRequest: timestamp({ withTimezone: true }),
|
|
109
|
-
/**
|
|
110
|
-
* Grants this key is confined to, in the roles' vocabulary; `null` means whatever
|
|
111
|
-
* the owner's role allows. Like `spaceIds`, a restriction only ever narrows.
|
|
112
|
-
*/
|
|
113
|
-
permissions: jsonb().$type<string[] | null>(),
|
|
114
|
-
/**
|
|
115
|
-
* Space ids this key may act in; `null` means every space the owner belongs to.
|
|
116
|
-
* A restriction only ever narrows the owner's own access, never widens it.
|
|
117
|
-
*/
|
|
118
|
-
spaceIds: jsonb().$type<string[] | null>(),
|
|
119
|
-
metadata: text(),
|
|
120
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
121
|
-
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
122
|
-
},
|
|
123
|
-
(table) => [index('apikeys_user_idx').on(table.userId)],
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
/** Space-scoped role assignment. */
|
|
127
|
-
export const memberships = pgTable(
|
|
128
|
-
'memberships',
|
|
129
|
-
{
|
|
130
|
-
userId: uuid()
|
|
131
|
-
.notNull()
|
|
132
|
-
.references(() => users.id, { onDelete: 'cascade' }),
|
|
133
|
-
spaceId: uuid()
|
|
134
|
-
.notNull()
|
|
135
|
-
.references(() => spaces.id, { onDelete: 'cascade' }),
|
|
136
|
-
/** The machine name of a built-in role or of a row in `roles` for the same space. */
|
|
137
|
-
role: text().notNull().default('editor'),
|
|
138
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
139
|
-
},
|
|
140
|
-
(table) => [
|
|
141
|
-
primaryKey({ columns: [table.userId, table.spaceId] }),
|
|
142
|
-
index('memberships_space_idx').on(table.spaceId),
|
|
143
|
-
],
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* A role someone created for a space, beside the built-in five. `permissions` holds the
|
|
148
|
-
* grants: `space:write`, `content:read` for every type, or `content:read:<typeId>` for
|
|
149
|
-
* one. Memberships name it by `machineName`, so a custom role is one row and one name.
|
|
150
|
-
*/
|
|
151
|
-
export const roles = pgTable(
|
|
152
|
-
'roles',
|
|
153
|
-
{
|
|
154
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
155
|
-
spaceId: uuid()
|
|
156
|
-
.notNull()
|
|
157
|
-
.references(() => spaces.id, { onDelete: 'cascade' }),
|
|
158
|
-
name: text().notNull(),
|
|
159
|
-
machineName: text().notNull(),
|
|
160
|
-
description: text(),
|
|
161
|
-
permissions: jsonb().$type<string[]>().notNull().default(sql`'[]'::jsonb`),
|
|
162
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
163
|
-
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
164
|
-
},
|
|
165
|
-
(table) => [uniqueIndex('roles_space_machine_name_key').on(table.spaceId, table.machineName)],
|
|
166
|
-
);
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { FieldDefinition } from '@manablox/core';
|
|
2
|
-
import { sql } from 'drizzle-orm';
|
|
3
|
-
import { boolean, index, jsonb, pgTable, text, timestamp, unique, uuid } from 'drizzle-orm/pg-core';
|
|
4
|
-
import { spaces } from './spaces.js';
|
|
5
|
-
|
|
6
|
-
/** Runtime-defined content types only; code-defined ones live in the registry. */
|
|
7
|
-
export const contentTypes = pgTable(
|
|
8
|
-
'content_types',
|
|
9
|
-
{
|
|
10
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
11
|
-
spaceId: uuid().references(() => spaces.id, { onDelete: 'cascade' }),
|
|
12
|
-
name: text().notNull(),
|
|
13
|
-
label: text().notNull(),
|
|
14
|
-
description: text(),
|
|
15
|
-
icon: text(),
|
|
16
|
-
kind: text().$type<'content' | 'block'>().notNull().default('content'),
|
|
17
|
-
hasSlug: boolean().notNull().default(true),
|
|
18
|
-
isPublishable: boolean().notNull().default(true),
|
|
19
|
-
isVisibleInTree: boolean().notNull().default(true),
|
|
20
|
-
canBeVisibleInMenu: boolean().notNull().default(true),
|
|
21
|
-
fields: jsonb().$type<FieldDefinition[]>().notNull().default(sql`'[]'::jsonb`),
|
|
22
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
23
|
-
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
24
|
-
},
|
|
25
|
-
(table) => [
|
|
26
|
-
// NULLS NOT DISTINCT so two global types cannot share a name — plain UNIQUE would
|
|
27
|
-
// treat every NULL space_id as distinct and let duplicates through.
|
|
28
|
-
unique('content_types_space_name_key').on(table.spaceId, table.name).nullsNotDistinct(),
|
|
29
|
-
index('content_types_space_idx').on(table.spaceId),
|
|
30
|
-
],
|
|
31
|
-
);
|
package/src/schema/content.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import type { ContentStatus } from '@manablox/core';
|
|
2
|
-
import { sql } from 'drizzle-orm';
|
|
3
|
-
import {
|
|
4
|
-
index,
|
|
5
|
-
integer,
|
|
6
|
-
jsonb,
|
|
7
|
-
pgTable,
|
|
8
|
-
text,
|
|
9
|
-
timestamp,
|
|
10
|
-
unique,
|
|
11
|
-
uniqueIndex,
|
|
12
|
-
uuid,
|
|
13
|
-
} from 'drizzle-orm/pg-core';
|
|
14
|
-
import { ltree, tsvector } from '../columns.js';
|
|
15
|
-
import { spaces } from './spaces.js';
|
|
16
|
-
|
|
17
|
-
/** Shared column set for `contents` and its published projection. */
|
|
18
|
-
const contentColumns = {
|
|
19
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
20
|
-
spaceId: uuid()
|
|
21
|
-
.notNull()
|
|
22
|
-
.references(() => spaces.id, { onDelete: 'cascade' }),
|
|
23
|
-
typeId: uuid().notNull(),
|
|
24
|
-
locale: text().notNull(),
|
|
25
|
-
/** Shared by all translations of one logical document. */
|
|
26
|
-
localizationId: uuid().notNull(),
|
|
27
|
-
parentId: uuid(),
|
|
28
|
-
title: text().notNull(),
|
|
29
|
-
slug: text().notNull(),
|
|
30
|
-
/** Materialised ancestor path, root first, ending in this node's own id. */
|
|
31
|
-
path: ltree().notNull(),
|
|
32
|
-
/** Routable URL path. NULL when the content type has no slug — such a node is a
|
|
33
|
-
* structural container, not a page, so it must not occupy a permalink of its own. */
|
|
34
|
-
permalink: text(),
|
|
35
|
-
/**
|
|
36
|
-
* Accumulated permalink prefix: every ancestor segment plus this node's own, with
|
|
37
|
-
* slug-less levels skipped. Descendants derive from this rather than from `permalink`,
|
|
38
|
-
* which is NULL for a slug-less node and would otherwise truncate the chain.
|
|
39
|
-
*/
|
|
40
|
-
permalinkPath: text().notNull().default(''),
|
|
41
|
-
/**
|
|
42
|
-
* This node's own contribution to its descendants' permalinks: the slug when the
|
|
43
|
-
* content type has one, NULL when it does not.
|
|
44
|
-
*
|
|
45
|
-
* Denormalised deliberately, so permalink recomputation is a single recursive CTE that
|
|
46
|
-
* needs no knowledge of the type registry.
|
|
47
|
-
*/
|
|
48
|
-
permalinkSegment: text(),
|
|
49
|
-
status: text().$type<ContentStatus>().notNull().default('draft'),
|
|
50
|
-
position: integer().notNull().default(0),
|
|
51
|
-
fields: jsonb().$type<Record<string, unknown>>().notNull().default(sql`'{}'::jsonb`),
|
|
52
|
-
/** Concatenated contributions from each field type's `search()`. */
|
|
53
|
-
searchText: text().notNull().default(''),
|
|
54
|
-
/** Optimistic-lock counter; a write with a stale version is rejected. */
|
|
55
|
-
version: integer().notNull().default(1),
|
|
56
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
57
|
-
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
58
|
-
createdBy: uuid(),
|
|
59
|
-
updatedBy: uuid(),
|
|
60
|
-
publishedAt: timestamp({ withTimezone: true }),
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
export const contents = pgTable(
|
|
64
|
-
'contents',
|
|
65
|
-
{
|
|
66
|
-
...contentColumns,
|
|
67
|
-
search: tsvector().generatedAlwaysAs(
|
|
68
|
-
sql`to_tsvector('simple', coalesce(title, '') || ' ' || coalesce(search_text, ''))`,
|
|
69
|
-
),
|
|
70
|
-
},
|
|
71
|
-
(table) => [
|
|
72
|
-
index('contents_path_gist_idx').using('gist', table.path),
|
|
73
|
-
index('contents_parent_idx').on(table.parentId),
|
|
74
|
-
index('contents_space_type_idx').on(table.spaceId, table.typeId),
|
|
75
|
-
index('contents_localization_idx').on(table.localizationId),
|
|
76
|
-
index('contents_status_idx').on(table.spaceId, table.status),
|
|
77
|
-
index('contents_fields_gin_idx').using('gin', sql`${table.fields} jsonb_path_ops`),
|
|
78
|
-
index('contents_search_gin_idx').using('gin', table.search),
|
|
79
|
-
// NULLS NOT DISTINCT so two root-level siblings cannot share a slug — with the
|
|
80
|
-
// default NULLS DISTINCT, every `parent_id IS NULL` row is unique by definition and
|
|
81
|
-
// the constraint would never fire at the top of the tree.
|
|
82
|
-
unique('contents_sibling_slug_key')
|
|
83
|
-
.on(table.spaceId, table.parentId, table.locale, table.slug)
|
|
84
|
-
.nullsNotDistinct(),
|
|
85
|
-
uniqueIndex('contents_permalink_key')
|
|
86
|
-
.on(table.spaceId, table.locale, table.permalink)
|
|
87
|
-
.where(sql`permalink is not null`),
|
|
88
|
-
],
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* The delivery projection. Written transactionally on publish, read by the public
|
|
93
|
-
* GraphQL API. Mirrors `contents` so a row can be copied column-for-column.
|
|
94
|
-
*/
|
|
95
|
-
export const publishedContents = pgTable(
|
|
96
|
-
'published_contents',
|
|
97
|
-
{
|
|
98
|
-
...contentColumns,
|
|
99
|
-
/** Version of `contents` this projection was made from. */
|
|
100
|
-
sourceVersion: integer().notNull().default(1),
|
|
101
|
-
search: tsvector().generatedAlwaysAs(
|
|
102
|
-
sql`to_tsvector('simple', coalesce(title, '') || ' ' || coalesce(search_text, ''))`,
|
|
103
|
-
),
|
|
104
|
-
},
|
|
105
|
-
(table) => [
|
|
106
|
-
index('published_contents_path_gist_idx').using('gist', table.path),
|
|
107
|
-
index('published_contents_parent_idx').on(table.parentId),
|
|
108
|
-
index('published_contents_space_type_idx').on(table.spaceId, table.typeId),
|
|
109
|
-
index('published_contents_fields_gin_idx').using('gin', sql`${table.fields} jsonb_path_ops`),
|
|
110
|
-
index('published_contents_search_gin_idx').using('gin', table.search),
|
|
111
|
-
uniqueIndex('published_contents_permalink_key')
|
|
112
|
-
.on(table.spaceId, table.locale, table.permalink)
|
|
113
|
-
.where(sql`permalink is not null`),
|
|
114
|
-
],
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
/** Full snapshot per save, so any edit can be rolled back. */
|
|
118
|
-
export const contentVersions = pgTable(
|
|
119
|
-
'content_versions',
|
|
120
|
-
{
|
|
121
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
122
|
-
contentId: uuid().notNull(),
|
|
123
|
-
version: integer().notNull(),
|
|
124
|
-
label: text(),
|
|
125
|
-
snapshot: jsonb().$type<Record<string, unknown>>().notNull(),
|
|
126
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
127
|
-
createdBy: uuid(),
|
|
128
|
-
},
|
|
129
|
-
(table) => [
|
|
130
|
-
uniqueIndex('content_versions_content_version_key').on(table.contentId, table.version),
|
|
131
|
-
index('content_versions_content_idx').on(table.contentId, table.createdAt),
|
|
132
|
-
],
|
|
133
|
-
);
|
package/src/schema/index.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The database schema, one file per entity. `drizzle.config.ts` points at this directory,
|
|
3
|
-
* and `relations.ts` ties the tables together so `db.query` can traverse them.
|
|
4
|
-
*/
|
|
5
|
-
export * from './assets.js';
|
|
6
|
-
export * from './auth.js';
|
|
7
|
-
export * from './content.js';
|
|
8
|
-
export * from './content-types.js';
|
|
9
|
-
export * from './menus.js';
|
|
10
|
-
export * from './relations.js';
|
|
11
|
-
export * from './spaces.js';
|
|
12
|
-
export * from './webhooks.js';
|
|
13
|
-
export * from './workflows.js';
|
|
14
|
-
|
|
15
|
-
import type { assets, assetUsages, assetVariants } from './assets.js';
|
|
16
|
-
import type { memberships, roles, users } from './auth.js';
|
|
17
|
-
import type { contents, contentVersions } from './content.js';
|
|
18
|
-
import type { contentTypes } from './content-types.js';
|
|
19
|
-
import type { menuItems, menus } from './menus.js';
|
|
20
|
-
import type { spaces } from './spaces.js';
|
|
21
|
-
import type { pushSubscriptions, workflowRuns, workflows } from './workflows.js';
|
|
22
|
-
|
|
23
|
-
export type SpaceRow = typeof spaces.$inferSelect;
|
|
24
|
-
export type ContentRow = typeof contents.$inferSelect;
|
|
25
|
-
export type ContentInsert = typeof contents.$inferInsert;
|
|
26
|
-
export type ContentTypeRow = typeof contentTypes.$inferSelect;
|
|
27
|
-
export type AssetRow = typeof assets.$inferSelect;
|
|
28
|
-
export type AssetVariantRow = typeof assetVariants.$inferSelect;
|
|
29
|
-
export type AssetUsageRow = typeof assetUsages.$inferSelect;
|
|
30
|
-
export type UserRow = typeof users.$inferSelect;
|
|
31
|
-
export type MembershipRow = typeof memberships.$inferSelect;
|
|
32
|
-
export type RoleRow = typeof roles.$inferSelect;
|
|
33
|
-
export type ContentVersionRow = typeof contentVersions.$inferSelect;
|
|
34
|
-
export type MenuRow = typeof menus.$inferSelect;
|
|
35
|
-
export type MenuItemRow = typeof menuItems.$inferSelect;
|
|
36
|
-
export type WorkflowRow = typeof workflows.$inferSelect;
|
|
37
|
-
export type WorkflowRunRow = typeof workflowRuns.$inferSelect;
|
|
38
|
-
export type PushSubscriptionRow = typeof pushSubscriptions.$inferSelect;
|
package/src/schema/menus.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AnyPgColumn,
|
|
3
|
-
index,
|
|
4
|
-
integer,
|
|
5
|
-
pgTable,
|
|
6
|
-
text,
|
|
7
|
-
timestamp,
|
|
8
|
-
unique,
|
|
9
|
-
uuid,
|
|
10
|
-
} from 'drizzle-orm/pg-core';
|
|
11
|
-
import { spaces } from './spaces.js';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A named navigation a site renders: "main", "footer", "legal". A document may sit in
|
|
15
|
-
* any number of them, which is why membership is a row here and not a flag on the
|
|
16
|
-
* document.
|
|
17
|
-
*/
|
|
18
|
-
export const menus = pgTable(
|
|
19
|
-
'menus',
|
|
20
|
-
{
|
|
21
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
22
|
-
spaceId: uuid()
|
|
23
|
-
.notNull()
|
|
24
|
-
.references(() => spaces.id, { onDelete: 'cascade' }),
|
|
25
|
-
name: text().notNull(),
|
|
26
|
-
/** What the delivery API is asked for: `menu(name: "main")`. */
|
|
27
|
-
machineName: text().notNull(),
|
|
28
|
-
description: text(),
|
|
29
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
30
|
-
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
31
|
-
},
|
|
32
|
-
(table) => [unique('menus_space_machine_name_key').on(table.spaceId, table.machineName)],
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* One entry of a menu, nested to any depth.
|
|
37
|
-
*
|
|
38
|
-
* A content entry points at the document's `localizationId` rather than at one row, so
|
|
39
|
-
* the same menu serves every locale: delivery resolves the translation for the locale
|
|
40
|
-
* it is asked for. A link entry has a `url` instead. Either may carry a `label`; for a
|
|
41
|
-
* content entry it overrides the document's title.
|
|
42
|
-
*/
|
|
43
|
-
export const menuItems = pgTable(
|
|
44
|
-
'menu_items',
|
|
45
|
-
{
|
|
46
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
47
|
-
menuId: uuid()
|
|
48
|
-
.notNull()
|
|
49
|
-
.references(() => menus.id, { onDelete: 'cascade' }),
|
|
50
|
-
/** Cascades, so removing an entry takes its sub-entries with it. */
|
|
51
|
-
parentId: uuid().references((): AnyPgColumn => menuItems.id, { onDelete: 'cascade' }),
|
|
52
|
-
position: integer().notNull().default(0),
|
|
53
|
-
localizationId: uuid(),
|
|
54
|
-
label: text(),
|
|
55
|
-
url: text(),
|
|
56
|
-
},
|
|
57
|
-
(table) => [
|
|
58
|
-
index('menu_items_menu_idx').on(table.menuId, table.parentId, table.position),
|
|
59
|
-
index('menu_items_localization_idx').on(table.localizationId),
|
|
60
|
-
],
|
|
61
|
-
);
|
package/src/schema/relations.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { relations } from 'drizzle-orm';
|
|
2
|
-
import { assets, assetUsages, assetVariants } from './assets.js';
|
|
3
|
-
import { memberships, roles, sessions, users } from './auth.js';
|
|
4
|
-
import { contents, contentVersions } from './content.js';
|
|
5
|
-
import { contentTypes } from './content-types.js';
|
|
6
|
-
import { menuItems, menus } from './menus.js';
|
|
7
|
-
import { spaces } from './spaces.js';
|
|
8
|
-
|
|
9
|
-
export const spacesRelations = relations(spaces, ({ many }) => ({
|
|
10
|
-
contents: many(contents),
|
|
11
|
-
contentTypes: many(contentTypes),
|
|
12
|
-
assets: many(assets),
|
|
13
|
-
memberships: many(memberships),
|
|
14
|
-
menus: many(menus),
|
|
15
|
-
roles: many(roles),
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
export const contentsRelations = relations(contents, ({ one, many }) => ({
|
|
19
|
-
space: one(spaces, { fields: [contents.spaceId], references: [spaces.id] }),
|
|
20
|
-
parent: one(contents, {
|
|
21
|
-
fields: [contents.parentId],
|
|
22
|
-
references: [contents.id],
|
|
23
|
-
relationName: 'tree',
|
|
24
|
-
}),
|
|
25
|
-
children: many(contents, { relationName: 'tree' }),
|
|
26
|
-
versions: many(contentVersions),
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
export const assetsRelations = relations(assets, ({ one, many }) => ({
|
|
30
|
-
space: one(spaces, { fields: [assets.spaceId], references: [spaces.id] }),
|
|
31
|
-
variants: many(assetVariants),
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
export const assetVariantsRelations = relations(assetVariants, ({ one }) => ({
|
|
35
|
-
asset: one(assets, { fields: [assetVariants.assetId], references: [assets.id] }),
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
export const assetUsagesRelations = relations(assetUsages, ({ one }) => ({
|
|
39
|
-
asset: one(assets, { fields: [assetUsages.assetId], references: [assets.id] }),
|
|
40
|
-
content: one(contents, { fields: [assetUsages.contentId], references: [contents.id] }),
|
|
41
|
-
}));
|
|
42
|
-
|
|
43
|
-
export const usersRelations = relations(users, ({ many }) => ({
|
|
44
|
-
sessions: many(sessions),
|
|
45
|
-
memberships: many(memberships),
|
|
46
|
-
}));
|
|
47
|
-
|
|
48
|
-
export const membershipsRelations = relations(memberships, ({ one }) => ({
|
|
49
|
-
user: one(users, { fields: [memberships.userId], references: [users.id] }),
|
|
50
|
-
space: one(spaces, { fields: [memberships.spaceId], references: [spaces.id] }),
|
|
51
|
-
}));
|
|
52
|
-
|
|
53
|
-
export const menusRelations = relations(menus, ({ one, many }) => ({
|
|
54
|
-
space: one(spaces, { fields: [menus.spaceId], references: [spaces.id] }),
|
|
55
|
-
items: many(menuItems),
|
|
56
|
-
}));
|
|
57
|
-
|
|
58
|
-
export const menuItemsRelations = relations(menuItems, ({ one }) => ({
|
|
59
|
-
menu: one(menus, { fields: [menuItems.menuId], references: [menus.id] }),
|
|
60
|
-
}));
|
|
61
|
-
|
|
62
|
-
export const rolesRelations = relations(roles, ({ one }) => ({
|
|
63
|
-
space: one(spaces, { fields: [roles.spaceId], references: [spaces.id] }),
|
|
64
|
-
}));
|
package/src/schema/spaces.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { sql } from 'drizzle-orm';
|
|
2
|
-
import { jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core';
|
|
3
|
-
|
|
4
|
-
export const spaces = pgTable(
|
|
5
|
-
'spaces',
|
|
6
|
-
{
|
|
7
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
8
|
-
name: text().notNull(),
|
|
9
|
-
machineName: text().notNull(),
|
|
10
|
-
description: text(),
|
|
11
|
-
/** Public origin of the space's frontend — used by the visual editor iframe. */
|
|
12
|
-
url: text().notNull(),
|
|
13
|
-
defaultLocale: text().notNull().default('en'),
|
|
14
|
-
locales: jsonb().$type<string[]>().notNull().default(sql`'["en"]'::jsonb`),
|
|
15
|
-
settings: jsonb().$type<Record<string, unknown>>().notNull().default(sql`'{}'::jsonb`),
|
|
16
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
17
|
-
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
18
|
-
},
|
|
19
|
-
(table) => [uniqueIndex('spaces_machine_name_key').on(table.machineName)],
|
|
20
|
-
);
|
package/src/schema/webhooks.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { sql } from 'drizzle-orm';
|
|
2
|
-
import {
|
|
3
|
-
boolean,
|
|
4
|
-
index,
|
|
5
|
-
integer,
|
|
6
|
-
jsonb,
|
|
7
|
-
pgTable,
|
|
8
|
-
text,
|
|
9
|
-
timestamp,
|
|
10
|
-
uuid,
|
|
11
|
-
} from 'drizzle-orm/pg-core';
|
|
12
|
-
import { spaces } from './spaces.js';
|
|
13
|
-
|
|
14
|
-
export const webhooks = pgTable(
|
|
15
|
-
'webhooks',
|
|
16
|
-
{
|
|
17
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
18
|
-
spaceId: uuid()
|
|
19
|
-
.notNull()
|
|
20
|
-
.references(() => spaces.id, { onDelete: 'cascade' }),
|
|
21
|
-
name: text().notNull(),
|
|
22
|
-
url: text().notNull(),
|
|
23
|
-
secret: text(),
|
|
24
|
-
events: jsonb().$type<string[]>().notNull().default(sql`'[]'::jsonb`),
|
|
25
|
-
enabled: boolean().notNull().default(true),
|
|
26
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
27
|
-
},
|
|
28
|
-
(table) => [index('webhooks_space_idx').on(table.spaceId)],
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
export const webhookDeliveries = pgTable(
|
|
32
|
-
'webhook_deliveries',
|
|
33
|
-
{
|
|
34
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
35
|
-
webhookId: uuid()
|
|
36
|
-
.notNull()
|
|
37
|
-
.references(() => webhooks.id, { onDelete: 'cascade' }),
|
|
38
|
-
event: text().notNull(),
|
|
39
|
-
payload: jsonb().$type<Record<string, unknown>>().notNull(),
|
|
40
|
-
status: integer(),
|
|
41
|
-
error: text(),
|
|
42
|
-
attempt: integer().notNull().default(1),
|
|
43
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
44
|
-
},
|
|
45
|
-
(table) => [index('webhook_deliveries_webhook_idx').on(table.webhookId, table.createdAt)],
|
|
46
|
-
);
|