@createcms/core 0.1.1
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 +169 -0
- package/dist/ab-edge/index.cjs +214 -0
- package/dist/ab-edge/index.d.cts +121 -0
- package/dist/ab-edge/index.d.ts +121 -0
- package/dist/ab-edge/index.js +205 -0
- package/dist/bin/createcms.js +3082 -0
- package/dist/db.cjs +496 -0
- package/dist/db.d.cts +128 -0
- package/dist/db.d.ts +128 -0
- package/dist/db.js +488 -0
- package/dist/index.cjs +13789 -0
- package/dist/index.d.cts +10277 -0
- package/dist/index.d.ts +10277 -0
- package/dist/index.js +13737 -0
- package/dist/nanoid.cjs +50 -0
- package/dist/nanoid.d.cts +29 -0
- package/dist/nanoid.d.ts +29 -0
- package/dist/nanoid.js +47 -0
- package/dist/next/index.cjs +60 -0
- package/dist/next/index.d.cts +141 -0
- package/dist/next/index.d.ts +141 -0
- package/dist/next/index.js +58 -0
- package/dist/next/middleware.cjs +113 -0
- package/dist/next/middleware.d.cts +77 -0
- package/dist/next/middleware.d.ts +77 -0
- package/dist/next/middleware.js +111 -0
- package/dist/plugins/ab-test/analytics/upstash.cjs +345 -0
- package/dist/plugins/ab-test/analytics/upstash.d.cts +193 -0
- package/dist/plugins/ab-test/analytics/upstash.d.ts +193 -0
- package/dist/plugins/ab-test/analytics/upstash.js +343 -0
- package/dist/plugins/ab-test/client.cjs +686 -0
- package/dist/plugins/ab-test/client.d.cts +233 -0
- package/dist/plugins/ab-test/client.d.ts +233 -0
- package/dist/plugins/ab-test/client.js +684 -0
- package/dist/plugins/ab-test/index.cjs +3400 -0
- package/dist/plugins/ab-test/index.d.cts +1131 -0
- package/dist/plugins/ab-test/index.d.ts +1131 -0
- package/dist/plugins/ab-test/index.js +3367 -0
- package/dist/plugins/client.cjs +20 -0
- package/dist/plugins/client.d.cts +3 -0
- package/dist/plugins/client.d.ts +3 -0
- package/dist/plugins/client.js +3 -0
- package/dist/plugins/consent/client.cjs +315 -0
- package/dist/plugins/consent/client.d.cts +145 -0
- package/dist/plugins/consent/client.d.ts +145 -0
- package/dist/plugins/consent/client.js +313 -0
- package/dist/plugins/consent/index.cjs +267 -0
- package/dist/plugins/consent/index.d.cts +618 -0
- package/dist/plugins/consent/index.d.ts +618 -0
- package/dist/plugins/consent/index.js +258 -0
- package/dist/plugins/i18n/index.cjs +2177 -0
- package/dist/plugins/i18n/index.d.cts +562 -0
- package/dist/plugins/i18n/index.d.ts +562 -0
- package/dist/plugins/i18n/index.js +2150 -0
- package/dist/plugins/media-optimize/index.cjs +315 -0
- package/dist/plugins/media-optimize/index.d.cts +144 -0
- package/dist/plugins/media-optimize/index.d.ts +144 -0
- package/dist/plugins/media-optimize/index.js +311 -0
- package/dist/plugins/multi-tenant/index.cjs +210 -0
- package/dist/plugins/multi-tenant/index.d.cts +431 -0
- package/dist/plugins/multi-tenant/index.d.ts +431 -0
- package/dist/plugins/multi-tenant/index.js +207 -0
- package/dist/plugins/server.cjs +24 -0
- package/dist/plugins/server.d.cts +3 -0
- package/dist/plugins/server.d.ts +3 -0
- package/dist/plugins/server.js +3 -0
- package/dist/react/blocks.cjs +233 -0
- package/dist/react/blocks.d.cts +320 -0
- package/dist/react/blocks.d.ts +320 -0
- package/dist/react/blocks.js +226 -0
- package/dist/react/index.cjs +901 -0
- package/dist/react/index.d.cts +992 -0
- package/dist/react/index.d.ts +992 -0
- package/dist/react/index.js +872 -0
- package/dist/react/tracking.cjs +243 -0
- package/dist/react/tracking.d.cts +364 -0
- package/dist/react/tracking.d.ts +364 -0
- package/dist/react/tracking.js +216 -0
- package/dist/react/variant.cjs +59 -0
- package/dist/react/variant.d.cts +26 -0
- package/dist/react/variant.d.ts +26 -0
- package/dist/react/variant.js +57 -0
- package/package.json +303 -0
|
@@ -0,0 +1,3082 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cac } from 'cac';
|
|
3
|
+
import kleur from 'kleur';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { mkdir, writeFile, access, readFile } from 'node:fs/promises';
|
|
6
|
+
import { createJiti } from 'jiti';
|
|
7
|
+
import { readFileSync, mkdirSync, writeFileSync, readdirSync } from 'node:fs';
|
|
8
|
+
import Module from 'node:module';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { pathToFileURL } from 'node:url';
|
|
11
|
+
import ora from 'ora';
|
|
12
|
+
import prompts from 'prompts';
|
|
13
|
+
|
|
14
|
+
function toSnakeCase(value) {
|
|
15
|
+
return value.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/[-\s]+/g, '_').toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
function cloneColumns(columns) {
|
|
18
|
+
return Object.fromEntries(Object.entries(columns).map(([key, value])=>[
|
|
19
|
+
key,
|
|
20
|
+
{
|
|
21
|
+
...value
|
|
22
|
+
}
|
|
23
|
+
]));
|
|
24
|
+
}
|
|
25
|
+
function cloneIndexes(indexes) {
|
|
26
|
+
return Object.fromEntries(Object.entries(indexes).map(([key, value])=>[
|
|
27
|
+
key,
|
|
28
|
+
{
|
|
29
|
+
...value,
|
|
30
|
+
columns: [
|
|
31
|
+
...value.columns
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
]));
|
|
35
|
+
}
|
|
36
|
+
function cloneEnum(enumDef) {
|
|
37
|
+
return {
|
|
38
|
+
...enumDef,
|
|
39
|
+
key: '',
|
|
40
|
+
dbName: '',
|
|
41
|
+
values: [
|
|
42
|
+
...enumDef.values
|
|
43
|
+
]
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function mergeSchemaSources(sources) {
|
|
47
|
+
const enums = {};
|
|
48
|
+
const tables = {};
|
|
49
|
+
for (const source of sources){
|
|
50
|
+
const sourceEnums = source.schema.enums ?? {};
|
|
51
|
+
for (const [enumKey, enumDef] of Object.entries(sourceEnums)){
|
|
52
|
+
if (enums[enumKey]) {
|
|
53
|
+
throw new Error(`Duplicate enum "${enumKey}" declared by "${source.name}".`);
|
|
54
|
+
}
|
|
55
|
+
const resolved = cloneEnum(enumDef);
|
|
56
|
+
resolved.key = enumKey;
|
|
57
|
+
resolved.dbName = enumDef.enumName ?? toSnakeCase(enumKey);
|
|
58
|
+
enums[enumKey] = resolved;
|
|
59
|
+
}
|
|
60
|
+
const sourceTables = source.schema.tables ?? {};
|
|
61
|
+
for (const [tableKey, tableDef] of Object.entries(sourceTables)){
|
|
62
|
+
if (tables[tableKey]) {
|
|
63
|
+
throw new Error(`Duplicate table "${tableKey}" declared by "${source.name}".`);
|
|
64
|
+
}
|
|
65
|
+
const resolved = {
|
|
66
|
+
key: tableKey,
|
|
67
|
+
dbName: tableDef.tableName ?? toSnakeCase(tableKey),
|
|
68
|
+
indexPrefix: tableDef.indexPrefix,
|
|
69
|
+
columns: cloneColumns(tableDef.columns),
|
|
70
|
+
indexes: cloneIndexes(Object.fromEntries(Object.entries(tableDef.indexes ?? {}).map(([indexKey, indexDef])=>[
|
|
71
|
+
indexKey,
|
|
72
|
+
{
|
|
73
|
+
...indexDef,
|
|
74
|
+
columns: [
|
|
75
|
+
...indexDef.columns
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
]))),
|
|
79
|
+
compositePrimaryKey: tableDef.compositePrimaryKey ? {
|
|
80
|
+
columns: [
|
|
81
|
+
...tableDef.compositePrimaryKey.columns
|
|
82
|
+
]
|
|
83
|
+
} : undefined,
|
|
84
|
+
foreignKeys: tableDef.foreignKeys ? tableDef.foreignKeys.map((fk)=>({
|
|
85
|
+
...fk,
|
|
86
|
+
columns: [
|
|
87
|
+
...fk.columns
|
|
88
|
+
],
|
|
89
|
+
foreignColumns: [
|
|
90
|
+
...fk.foreignColumns
|
|
91
|
+
]
|
|
92
|
+
})) : undefined
|
|
93
|
+
};
|
|
94
|
+
tables[tableKey] = resolved;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Phase 2: Apply extensions
|
|
98
|
+
for (const source of sources){
|
|
99
|
+
const sourceExtensions = source.schema.extend ?? {};
|
|
100
|
+
for (const [targetTableKey, extension] of Object.entries(sourceExtensions)){
|
|
101
|
+
const targetTable = tables[targetTableKey];
|
|
102
|
+
if (!targetTable) {
|
|
103
|
+
throw new Error(`Schema source "${source.name}" extends unknown table "${targetTableKey}".`);
|
|
104
|
+
}
|
|
105
|
+
for (const [columnKey, columnDef] of Object.entries(extension.columns)){
|
|
106
|
+
if (targetTable.columns[columnKey]) {
|
|
107
|
+
throw new Error(`Schema source "${source.name}" tried to add duplicate column "${columnKey}" to "${targetTableKey}".`);
|
|
108
|
+
}
|
|
109
|
+
targetTable.columns[columnKey] = {
|
|
110
|
+
...columnDef
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
for (const [indexKey, indexDef] of Object.entries(extension.indexes ?? {})){
|
|
114
|
+
if (targetTable.indexes[indexKey]) {
|
|
115
|
+
throw new Error(`Schema source "${source.name}" tried to add duplicate index "${indexKey}" to "${targetTableKey}".`);
|
|
116
|
+
}
|
|
117
|
+
targetTable.indexes[indexKey] = {
|
|
118
|
+
...indexDef,
|
|
119
|
+
columns: [
|
|
120
|
+
...indexDef.columns
|
|
121
|
+
]
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Phase 3: Validation
|
|
127
|
+
const enumDbNames = new Set();
|
|
128
|
+
for (const enumDef of Object.values(enums)){
|
|
129
|
+
if (enumDbNames.has(enumDef.dbName)) {
|
|
130
|
+
throw new Error(`Duplicate enum database name "${enumDef.dbName}".`);
|
|
131
|
+
}
|
|
132
|
+
enumDbNames.add(enumDef.dbName);
|
|
133
|
+
}
|
|
134
|
+
const tableDbNames = new Set();
|
|
135
|
+
for (const table of Object.values(tables)){
|
|
136
|
+
if (tableDbNames.has(table.dbName)) {
|
|
137
|
+
throw new Error(`Duplicate table database name "${table.dbName}".`);
|
|
138
|
+
}
|
|
139
|
+
tableDbNames.add(table.dbName);
|
|
140
|
+
for (const [columnKey, columnDef] of Object.entries(table.columns)){
|
|
141
|
+
if (typeof columnDef.type === 'object') {
|
|
142
|
+
const enumName = columnDef.type.enum;
|
|
143
|
+
if (!enums[enumName]) {
|
|
144
|
+
throw new Error(`Column "${table.key}.${columnKey}" references unknown enum "${enumName}".`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (columnDef.references) {
|
|
148
|
+
const referencedTable = tables[columnDef.references.table];
|
|
149
|
+
if (!referencedTable) {
|
|
150
|
+
throw new Error(`Column "${table.key}.${columnKey}" references unknown table "${columnDef.references.table}".`);
|
|
151
|
+
}
|
|
152
|
+
if (!referencedTable.columns[columnDef.references.column]) {
|
|
153
|
+
throw new Error(`Column "${table.key}.${columnKey}" references unknown column "${columnDef.references.table}.${columnDef.references.column}".`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const [indexKey, indexDef] of Object.entries(table.indexes)){
|
|
158
|
+
for (const columnName of indexDef.columns){
|
|
159
|
+
if (!table.columns[columnName]) {
|
|
160
|
+
throw new Error(`Index "${table.key}.${indexKey}" references unknown column "${columnName}".`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (table.foreignKeys) {
|
|
165
|
+
for (const fk of table.foreignKeys){
|
|
166
|
+
for (const col of fk.columns){
|
|
167
|
+
if (!table.columns[col]) {
|
|
168
|
+
throw new Error(`Foreign key in "${table.key}" references unknown local column "${col}".`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const refTable = tables[fk.foreignTable];
|
|
172
|
+
if (!refTable) {
|
|
173
|
+
throw new Error(`Foreign key in "${table.key}" references unknown table "${fk.foreignTable}".`);
|
|
174
|
+
}
|
|
175
|
+
for (const col of fk.foreignColumns){
|
|
176
|
+
if (!refTable.columns[col]) {
|
|
177
|
+
throw new Error(`Foreign key in "${table.key}" references unknown column "${fk.foreignTable}.${col}".`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
enums,
|
|
185
|
+
tables
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function quote(value) {
|
|
190
|
+
return JSON.stringify(value);
|
|
191
|
+
}
|
|
192
|
+
function toPascalCase(value) {
|
|
193
|
+
return value.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_\-\s]+/g, ' ').split(' ').filter(Boolean).map((part)=>part.charAt(0).toUpperCase() + part.slice(1)).join('');
|
|
194
|
+
}
|
|
195
|
+
function singularize(value) {
|
|
196
|
+
if (value.endsWith('ies')) {
|
|
197
|
+
return `${value.slice(0, -3)}y`;
|
|
198
|
+
}
|
|
199
|
+
if (value.endsWith('ches') || value.endsWith('shes') || value.endsWith('xes') || value.endsWith('zes')) {
|
|
200
|
+
return value.slice(0, -2);
|
|
201
|
+
}
|
|
202
|
+
if (value.endsWith('s') && !value.endsWith('ss')) {
|
|
203
|
+
return value.slice(0, -1);
|
|
204
|
+
}
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
function emitLiteral(value) {
|
|
208
|
+
return JSON.stringify(value.value);
|
|
209
|
+
}
|
|
210
|
+
function emitDefault(defaultValue) {
|
|
211
|
+
switch(defaultValue.kind){
|
|
212
|
+
case 'literal':
|
|
213
|
+
return {
|
|
214
|
+
code: `.default(${emitLiteral(defaultValue)})`,
|
|
215
|
+
needsSql: false
|
|
216
|
+
};
|
|
217
|
+
case 'sql':
|
|
218
|
+
return {
|
|
219
|
+
code: `.default(sql.raw(${quote(defaultValue.value)}))`,
|
|
220
|
+
needsSql: true
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function emitEnum(enumDef) {
|
|
225
|
+
const values = `[${enumDef.values.map(quote).join(', ')}]`;
|
|
226
|
+
return `export const ${enumDef.key}Enum = cms.enum(${quote(enumDef.dbName)}, ${values});`;
|
|
227
|
+
}
|
|
228
|
+
function emitColumn(table, columnKey, columnDef) {
|
|
229
|
+
const columnName = columnDef.columnName ?? toSnakeCase(columnKey);
|
|
230
|
+
let builder = '';
|
|
231
|
+
let needsTsvector = false;
|
|
232
|
+
if (typeof columnDef.type === 'string') {
|
|
233
|
+
if (columnDef.type === 'tsvector') {
|
|
234
|
+
builder = `tsvectorColumn(${quote(columnName)})`;
|
|
235
|
+
needsTsvector = true;
|
|
236
|
+
} else {
|
|
237
|
+
builder = `${columnDef.type}(${quote(columnName)})`;
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
builder = `${columnDef.type.enum}Enum(${quote(columnName)})`;
|
|
241
|
+
}
|
|
242
|
+
let needsSql = false;
|
|
243
|
+
let needsAnyPgColumn = false;
|
|
244
|
+
if (columnDef.jsonType && typeof columnDef.type === 'string' && columnDef.type === 'jsonb') {
|
|
245
|
+
builder += `.$type<${columnDef.jsonType}>()`;
|
|
246
|
+
}
|
|
247
|
+
if (columnDef.primaryKey) {
|
|
248
|
+
builder += '.primaryKey()';
|
|
249
|
+
}
|
|
250
|
+
if (columnDef.notNull) {
|
|
251
|
+
builder += '.notNull()';
|
|
252
|
+
}
|
|
253
|
+
if (columnDef.unique) {
|
|
254
|
+
builder += '.unique()';
|
|
255
|
+
}
|
|
256
|
+
if (columnDef.defaultNow) {
|
|
257
|
+
builder += '.defaultNow()';
|
|
258
|
+
}
|
|
259
|
+
if (columnDef.defaultId) {
|
|
260
|
+
if (!columnDef.defaultIdPrefix) {
|
|
261
|
+
throw new Error(`Column "${columnKey}" has defaultId but no defaultIdPrefix`);
|
|
262
|
+
}
|
|
263
|
+
builder += `.$defaultFn(() => newId(${quote(columnDef.defaultIdPrefix)}))`;
|
|
264
|
+
}
|
|
265
|
+
if (columnDef.default) {
|
|
266
|
+
const emitted = emitDefault(columnDef.default);
|
|
267
|
+
builder += emitted.code;
|
|
268
|
+
needsSql ||= emitted.needsSql;
|
|
269
|
+
}
|
|
270
|
+
if (columnDef.references) {
|
|
271
|
+
const options = [];
|
|
272
|
+
if (columnDef.references.onDelete) {
|
|
273
|
+
options.push(`onDelete: ${quote(columnDef.references.onDelete)}`);
|
|
274
|
+
}
|
|
275
|
+
if (columnDef.references.onUpdate) {
|
|
276
|
+
options.push(`onUpdate: ${quote(columnDef.references.onUpdate)}`);
|
|
277
|
+
}
|
|
278
|
+
const suffix = options.length > 0 ? `, { ${options.join(', ')} }` : '';
|
|
279
|
+
const isSelfReference = columnDef.references.table === table.key;
|
|
280
|
+
const refCallback = isSelfReference ? `(): AnyPgColumn => ${columnDef.references.table}.${columnDef.references.column}` : `() => ${columnDef.references.table}.${columnDef.references.column}`;
|
|
281
|
+
needsAnyPgColumn ||= isSelfReference;
|
|
282
|
+
builder += `.references(${refCallback}${suffix})`;
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
code: ` ${columnKey}: ${builder},`,
|
|
286
|
+
needsSql,
|
|
287
|
+
needsAnyPgColumn,
|
|
288
|
+
needsTsvector
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function emitIndexes(table) {
|
|
292
|
+
const entries = Object.entries(table.indexes);
|
|
293
|
+
if (entries.length === 0) {
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
const prefix = table.indexPrefix ?? table.dbName;
|
|
297
|
+
return entries.map(([indexKey, indexDef])=>{
|
|
298
|
+
const factory = indexDef.unique ? 'uniqueIndex' : 'index';
|
|
299
|
+
const columns = indexDef.columns.map((column)=>`table.${column}`).join(', ');
|
|
300
|
+
let line;
|
|
301
|
+
if (indexDef.using && indexDef.using !== 'btree') {
|
|
302
|
+
line = ` ${factory}(${quote(`${prefix}_${toSnakeCase(indexKey)}`)}).using(${quote(indexDef.using)}, ${columns})`;
|
|
303
|
+
} else {
|
|
304
|
+
line = ` ${factory}(${quote(`${prefix}_${toSnakeCase(indexKey)}`)}).on(${columns})`;
|
|
305
|
+
}
|
|
306
|
+
if (indexDef.where) {
|
|
307
|
+
line += `.where(sql\`${indexDef.where}\`)`;
|
|
308
|
+
}
|
|
309
|
+
return `${line},`;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function emitCompositePrimaryKey(table) {
|
|
313
|
+
if (!table.compositePrimaryKey) return null;
|
|
314
|
+
const columns = table.compositePrimaryKey.columns.map((col)=>`table.${col}`).join(', ');
|
|
315
|
+
return ` primaryKey({ columns: [${columns}] }),`;
|
|
316
|
+
}
|
|
317
|
+
function emitTableLevelForeignKeys(table) {
|
|
318
|
+
if (!table.foreignKeys || table.foreignKeys.length === 0) {
|
|
319
|
+
return {
|
|
320
|
+
lines: [],
|
|
321
|
+
needsForeignKeyImport: false
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const lines = table.foreignKeys.map((fk)=>{
|
|
325
|
+
const localCols = fk.columns.map((c)=>`table.${c}`).join(', ');
|
|
326
|
+
const isSelfReference = fk.foreignTable === table.key;
|
|
327
|
+
const foreignCols = fk.foreignColumns.map((c)=>`${isSelfReference ? 'table' : fk.foreignTable}.${c}`).join(', ');
|
|
328
|
+
const parts = [
|
|
329
|
+
` columns: [${localCols}],`,
|
|
330
|
+
` foreignColumns: [${foreignCols}],`
|
|
331
|
+
];
|
|
332
|
+
if (fk.name) {
|
|
333
|
+
parts.push(` name: ${quote(fk.name)},`);
|
|
334
|
+
}
|
|
335
|
+
let chain = '';
|
|
336
|
+
if (fk.onDelete) {
|
|
337
|
+
chain += `.onDelete(${quote(fk.onDelete)})`;
|
|
338
|
+
}
|
|
339
|
+
if (fk.onUpdate) {
|
|
340
|
+
chain += `.onUpdate(${quote(fk.onUpdate)})`;
|
|
341
|
+
}
|
|
342
|
+
return ` foreignKey({\n${parts.join('\n')}\n })${chain},`;
|
|
343
|
+
});
|
|
344
|
+
return {
|
|
345
|
+
lines,
|
|
346
|
+
needsForeignKeyImport: true
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function emitTableTypeAliases(table) {
|
|
350
|
+
const baseName = toPascalCase(singularize(table.key));
|
|
351
|
+
return `export type ${baseName} = typeof ${table.key}.$inferSelect;
|
|
352
|
+
export type New${baseName} = typeof ${table.key}.$inferInsert;`;
|
|
353
|
+
}
|
|
354
|
+
function emitTable(table) {
|
|
355
|
+
let needsSql = false;
|
|
356
|
+
let needsAnyPgColumn = false;
|
|
357
|
+
let needsForeignKeyImport = false;
|
|
358
|
+
let needsPrimaryKeyImport = false;
|
|
359
|
+
let needsTsvector = false;
|
|
360
|
+
const emittedColumns = Object.entries(table.columns).map(([columnKey, columnDef])=>{
|
|
361
|
+
const emitted = emitColumn(table, columnKey, columnDef);
|
|
362
|
+
needsSql ||= emitted.needsSql;
|
|
363
|
+
needsAnyPgColumn ||= emitted.needsAnyPgColumn;
|
|
364
|
+
needsTsvector ||= emitted.needsTsvector;
|
|
365
|
+
return emitted.code;
|
|
366
|
+
});
|
|
367
|
+
const indexes = emitIndexes(table);
|
|
368
|
+
for (const idx of Object.values(table.indexes)){
|
|
369
|
+
if (idx.where) needsSql = true;
|
|
370
|
+
}
|
|
371
|
+
const compositePK = emitCompositePrimaryKey(table);
|
|
372
|
+
if (compositePK) needsPrimaryKeyImport = true;
|
|
373
|
+
const { lines: fkLines, needsForeignKeyImport: needsFK } = emitTableLevelForeignKeys(table);
|
|
374
|
+
needsForeignKeyImport = needsFK;
|
|
375
|
+
const tableFactory = 'cms.table';
|
|
376
|
+
const callbackLines = [
|
|
377
|
+
...compositePK ? [
|
|
378
|
+
compositePK
|
|
379
|
+
] : [],
|
|
380
|
+
...fkLines,
|
|
381
|
+
...indexes
|
|
382
|
+
];
|
|
383
|
+
const callback = callbackLines.length > 0 ? `,
|
|
384
|
+
(table) => [
|
|
385
|
+
${callbackLines.join('\n')}
|
|
386
|
+
]` : '';
|
|
387
|
+
return {
|
|
388
|
+
code: `export const ${table.key} = ${tableFactory}(
|
|
389
|
+
${quote(table.dbName)},
|
|
390
|
+
{
|
|
391
|
+
${emittedColumns.join('\n')}
|
|
392
|
+
}${callback},
|
|
393
|
+
);`,
|
|
394
|
+
needsSql,
|
|
395
|
+
needsAnyPgColumn,
|
|
396
|
+
needsForeignKeyImport,
|
|
397
|
+
needsPrimaryKeyImport,
|
|
398
|
+
needsTsvector
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function emitDrizzleSchema(schema, options) {
|
|
402
|
+
const pgCoreImports = new Set([
|
|
403
|
+
'pgSchema'
|
|
404
|
+
]);
|
|
405
|
+
let needsSql = false;
|
|
406
|
+
let needsAnyPgColumn = false;
|
|
407
|
+
let needsForeignKeyImport = false;
|
|
408
|
+
let needsPrimaryKeyImport = false;
|
|
409
|
+
let needsTsvector = false;
|
|
410
|
+
const enums = Object.values(schema.enums).sort((a, b)=>a.key.localeCompare(b.key));
|
|
411
|
+
const tables = Object.values(schema.tables).sort((a, b)=>a.key.localeCompare(b.key));
|
|
412
|
+
for (const table of tables){
|
|
413
|
+
for (const column of Object.values(table.columns)){
|
|
414
|
+
if (typeof column.type === 'string') {
|
|
415
|
+
if (column.type !== 'tsvector') {
|
|
416
|
+
pgCoreImports.add(column.type);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (column.default?.kind === 'sql') {
|
|
420
|
+
needsSql = true;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (Object.keys(table.indexes).length > 0) {
|
|
424
|
+
pgCoreImports.add('index');
|
|
425
|
+
}
|
|
426
|
+
if (Object.values(table.indexes).some((indexDef)=>indexDef.unique)) {
|
|
427
|
+
pgCoreImports.add('uniqueIndex');
|
|
428
|
+
}
|
|
429
|
+
if (Object.values(table.indexes).some((indexDef)=>indexDef.where)) {
|
|
430
|
+
needsSql = true;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const enumBlocks = enums.map(emitEnum);
|
|
434
|
+
const tableBlocks = tables.map((table)=>{
|
|
435
|
+
const emitted = emitTable(table);
|
|
436
|
+
needsSql ||= emitted.needsSql;
|
|
437
|
+
needsAnyPgColumn ||= emitted.needsAnyPgColumn;
|
|
438
|
+
needsForeignKeyImport ||= emitted.needsForeignKeyImport;
|
|
439
|
+
needsPrimaryKeyImport ||= emitted.needsPrimaryKeyImport;
|
|
440
|
+
needsTsvector ||= emitted.needsTsvector;
|
|
441
|
+
return emitted.code;
|
|
442
|
+
});
|
|
443
|
+
const typeBlocks = tables.map(emitTableTypeAliases);
|
|
444
|
+
if (needsForeignKeyImport) pgCoreImports.add('foreignKey');
|
|
445
|
+
if (needsPrimaryKeyImport) pgCoreImports.add('primaryKey');
|
|
446
|
+
if (needsTsvector) pgCoreImports.add('customType');
|
|
447
|
+
const imports = [];
|
|
448
|
+
if (needsSql) {
|
|
449
|
+
imports.push(`import { sql } from 'drizzle-orm';`);
|
|
450
|
+
}
|
|
451
|
+
imports.push(`import { ${Array.from(pgCoreImports).sort().join(', ')} } from 'drizzle-orm/pg-core';`);
|
|
452
|
+
if (needsAnyPgColumn) {
|
|
453
|
+
imports.push(`import type { AnyPgColumn } from 'drizzle-orm/pg-core';`);
|
|
454
|
+
}
|
|
455
|
+
const needsNewId = tables.some((table)=>Object.values(table.columns).some((col)=>col.defaultId));
|
|
456
|
+
if (needsNewId) {
|
|
457
|
+
imports.push(options?.nanoidImport ?? `import { newId } from '@createcms/core/nanoid';`);
|
|
458
|
+
}
|
|
459
|
+
const tsvectorDef = needsTsvector ? `\nconst tsvectorColumn = customType<{ data: string }>({
|
|
460
|
+
dataType() { return 'tsvector'; },
|
|
461
|
+
});\n` : '';
|
|
462
|
+
const schemaExport = `export const schema = {
|
|
463
|
+
${tables.map((table)=>` ${table.key},`).join('\n')}
|
|
464
|
+
${enums.map((enumDef)=>` ${enumDef.key}Enum,`).join('\n')}
|
|
465
|
+
};`;
|
|
466
|
+
return `/* eslint-disable */
|
|
467
|
+
/**
|
|
468
|
+
* This file is generated by createcms.
|
|
469
|
+
* Do not edit manually.
|
|
470
|
+
*/
|
|
471
|
+
|
|
472
|
+
${imports.join('\n')}
|
|
473
|
+
|
|
474
|
+
export const cms = pgSchema('cms');
|
|
475
|
+
${tsvectorDef}${enumBlocks.join('\n\n')}
|
|
476
|
+
${enumBlocks.length > 0 && tableBlocks.length > 0 ? '\n' : ''}${tableBlocks.join('\n\n')}
|
|
477
|
+
|
|
478
|
+
${schemaExport}
|
|
479
|
+
|
|
480
|
+
${typeBlocks.join('\n\n')}
|
|
481
|
+
`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function generateSchema(config) {
|
|
485
|
+
const merged = mergeSchemaSources(config.sources);
|
|
486
|
+
const output = emitDrizzleSchema(merged, config.emit);
|
|
487
|
+
const outputPath = path.resolve(config.outputPath);
|
|
488
|
+
await mkdir(path.dirname(outputPath), {
|
|
489
|
+
recursive: true
|
|
490
|
+
});
|
|
491
|
+
await writeFile(outputPath, output, 'utf8');
|
|
492
|
+
return {
|
|
493
|
+
outputPath,
|
|
494
|
+
schema: merged,
|
|
495
|
+
output
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function defineCoreSchema(schema) {
|
|
500
|
+
return {
|
|
501
|
+
...schema
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const coreSchema = defineCoreSchema({
|
|
506
|
+
enums: {
|
|
507
|
+
approvalStatus: {
|
|
508
|
+
enumName: 'approval_status',
|
|
509
|
+
values: [
|
|
510
|
+
'pending',
|
|
511
|
+
'approved',
|
|
512
|
+
'rejected'
|
|
513
|
+
]
|
|
514
|
+
},
|
|
515
|
+
assetStatus: {
|
|
516
|
+
enumName: 'asset_status',
|
|
517
|
+
values: [
|
|
518
|
+
'private',
|
|
519
|
+
'public'
|
|
520
|
+
]
|
|
521
|
+
},
|
|
522
|
+
mergeRequestStatus: {
|
|
523
|
+
enumName: 'merge_request_status',
|
|
524
|
+
values: [
|
|
525
|
+
'open',
|
|
526
|
+
'merged',
|
|
527
|
+
'closed'
|
|
528
|
+
]
|
|
529
|
+
},
|
|
530
|
+
conflictResolution: {
|
|
531
|
+
enumName: 'conflict_resolution',
|
|
532
|
+
values: [
|
|
533
|
+
'source',
|
|
534
|
+
'target',
|
|
535
|
+
'manual'
|
|
536
|
+
]
|
|
537
|
+
},
|
|
538
|
+
commentThreadTarget: {
|
|
539
|
+
enumName: 'comment_thread_target',
|
|
540
|
+
values: [
|
|
541
|
+
'mergeRequest',
|
|
542
|
+
'block'
|
|
543
|
+
]
|
|
544
|
+
},
|
|
545
|
+
commentThreadStatus: {
|
|
546
|
+
enumName: 'comment_thread_status',
|
|
547
|
+
values: [
|
|
548
|
+
'open',
|
|
549
|
+
'resolved'
|
|
550
|
+
]
|
|
551
|
+
},
|
|
552
|
+
commentMessageType: {
|
|
553
|
+
enumName: 'comment_message_type',
|
|
554
|
+
values: [
|
|
555
|
+
'comment',
|
|
556
|
+
'system'
|
|
557
|
+
]
|
|
558
|
+
},
|
|
559
|
+
commentSystemType: {
|
|
560
|
+
enumName: 'comment_system_type',
|
|
561
|
+
values: [
|
|
562
|
+
'threadResolved',
|
|
563
|
+
'threadReopened'
|
|
564
|
+
]
|
|
565
|
+
},
|
|
566
|
+
notificationType: {
|
|
567
|
+
enumName: 'notification_type',
|
|
568
|
+
values: [
|
|
569
|
+
'mention',
|
|
570
|
+
'comment',
|
|
571
|
+
'threadResolved',
|
|
572
|
+
'approvalRequested',
|
|
573
|
+
'approvalApproved',
|
|
574
|
+
'approvalRejected',
|
|
575
|
+
'mergeRequestOpened',
|
|
576
|
+
'mergeRequestMerged',
|
|
577
|
+
'mergeRequestClosed',
|
|
578
|
+
'mergeRequestReopened',
|
|
579
|
+
'published',
|
|
580
|
+
'custom'
|
|
581
|
+
]
|
|
582
|
+
},
|
|
583
|
+
// A redirect endpoint (source or target) is either a page REFERENCE (rootId,
|
|
584
|
+
// resolves to the page's current path — follows moves) or a literal PATH.
|
|
585
|
+
// 'regex' is reserved for a later version (unindexed ordered scan).
|
|
586
|
+
redirectEndpointType: {
|
|
587
|
+
enumName: 'redirect_endpoint_type',
|
|
588
|
+
values: [
|
|
589
|
+
'page',
|
|
590
|
+
'path'
|
|
591
|
+
]
|
|
592
|
+
},
|
|
593
|
+
// The kind of content a `content_usages` row indexes. One generalist index
|
|
594
|
+
// (version-keyed, insert-only, branch-head liveness) backs all three: assets
|
|
595
|
+
// (GC reclaim + media-library UI), variables (in-use guard + revalidation),
|
|
596
|
+
// and reusable-block references (delete guard + usage UI). 'reference' is
|
|
597
|
+
// populated from RB1 on; the enum carries it now so RB1 needs no enum change.
|
|
598
|
+
contentUsageTarget: {
|
|
599
|
+
enumName: 'content_usage_target',
|
|
600
|
+
values: [
|
|
601
|
+
'asset',
|
|
602
|
+
'variable',
|
|
603
|
+
'reference'
|
|
604
|
+
]
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
tables: {
|
|
608
|
+
// ========================================================================
|
|
609
|
+
// ROOTS
|
|
610
|
+
// ========================================================================
|
|
611
|
+
roots: {
|
|
612
|
+
columns: {
|
|
613
|
+
id: {
|
|
614
|
+
type: 'text',
|
|
615
|
+
primaryKey: true,
|
|
616
|
+
defaultId: true,
|
|
617
|
+
defaultIdPrefix: 'root'
|
|
618
|
+
},
|
|
619
|
+
collection: {
|
|
620
|
+
type: 'text',
|
|
621
|
+
notNull: true
|
|
622
|
+
},
|
|
623
|
+
parentRootId: {
|
|
624
|
+
type: 'text'
|
|
625
|
+
},
|
|
626
|
+
slug: {
|
|
627
|
+
type: 'text'
|
|
628
|
+
},
|
|
629
|
+
sortOrder: {
|
|
630
|
+
type: 'integer',
|
|
631
|
+
notNull: true,
|
|
632
|
+
default: {
|
|
633
|
+
kind: 'literal',
|
|
634
|
+
value: 0
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
createdBy: {
|
|
638
|
+
type: 'text'
|
|
639
|
+
},
|
|
640
|
+
createdAt: {
|
|
641
|
+
type: 'timestamp',
|
|
642
|
+
notNull: true,
|
|
643
|
+
defaultNow: true
|
|
644
|
+
},
|
|
645
|
+
archivedAt: {
|
|
646
|
+
type: 'timestamp'
|
|
647
|
+
},
|
|
648
|
+
lastPrunedAt: {
|
|
649
|
+
type: 'timestamp'
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
foreignKeys: [
|
|
653
|
+
{
|
|
654
|
+
columns: [
|
|
655
|
+
'parentRootId'
|
|
656
|
+
],
|
|
657
|
+
foreignTable: 'roots',
|
|
658
|
+
foreignColumns: [
|
|
659
|
+
'id'
|
|
660
|
+
],
|
|
661
|
+
name: 'roots_parent_fk',
|
|
662
|
+
onDelete: 'cascade'
|
|
663
|
+
}
|
|
664
|
+
],
|
|
665
|
+
indexes: {
|
|
666
|
+
collectionIdx: {
|
|
667
|
+
columns: [
|
|
668
|
+
'collection'
|
|
669
|
+
]
|
|
670
|
+
},
|
|
671
|
+
parentRootIdx: {
|
|
672
|
+
columns: [
|
|
673
|
+
'parentRootId'
|
|
674
|
+
]
|
|
675
|
+
},
|
|
676
|
+
// Lookup index for resolvePathToRootId's slug-chain CTE. NON-unique on
|
|
677
|
+
// purpose: a core GLOBAL unique on (collection, parentRootId, slug) cannot
|
|
678
|
+
// be loosened by a plugin (merge.ts only ADDS), so it would forbid the
|
|
679
|
+
// i18n plugin from allowing the SAME slug across languages (en/blog +
|
|
680
|
+
// de/blog). Uniqueness is the app-level authority (validateSlugUniqueness,
|
|
681
|
+
// called on every slug write) plus, under a scoping plugin, a per-scope
|
|
682
|
+
// partial unique. See I18N_DESIGN.md §3 (the redirects "Option B" move).
|
|
683
|
+
slugIdx: {
|
|
684
|
+
columns: [
|
|
685
|
+
'collection',
|
|
686
|
+
'parentRootId',
|
|
687
|
+
'slug'
|
|
688
|
+
]
|
|
689
|
+
},
|
|
690
|
+
// Pruning GC round-robin: pick the least-recently-pruned live roots.
|
|
691
|
+
archivedAtIdx: {
|
|
692
|
+
columns: [
|
|
693
|
+
'archivedAt'
|
|
694
|
+
]
|
|
695
|
+
},
|
|
696
|
+
lastPrunedAtIdx: {
|
|
697
|
+
columns: [
|
|
698
|
+
'lastPrunedAt'
|
|
699
|
+
]
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
// ========================================================================
|
|
704
|
+
// COMMITS
|
|
705
|
+
// ========================================================================
|
|
706
|
+
commits: {
|
|
707
|
+
columns: {
|
|
708
|
+
id: {
|
|
709
|
+
type: 'text',
|
|
710
|
+
primaryKey: true,
|
|
711
|
+
defaultId: true,
|
|
712
|
+
defaultIdPrefix: 'commit'
|
|
713
|
+
},
|
|
714
|
+
rootId: {
|
|
715
|
+
type: 'text',
|
|
716
|
+
notNull: true,
|
|
717
|
+
references: {
|
|
718
|
+
table: 'roots',
|
|
719
|
+
column: 'id'
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
parentCommitId: {
|
|
723
|
+
type: 'text'
|
|
724
|
+
},
|
|
725
|
+
mergeSourceCommitId: {
|
|
726
|
+
type: 'text'
|
|
727
|
+
},
|
|
728
|
+
message: {
|
|
729
|
+
type: 'text'
|
|
730
|
+
},
|
|
731
|
+
createdBy: {
|
|
732
|
+
type: 'text'
|
|
733
|
+
},
|
|
734
|
+
createdAt: {
|
|
735
|
+
type: 'timestamp',
|
|
736
|
+
notNull: true,
|
|
737
|
+
defaultNow: true
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
foreignKeys: [
|
|
741
|
+
{
|
|
742
|
+
columns: [
|
|
743
|
+
'parentCommitId'
|
|
744
|
+
],
|
|
745
|
+
foreignTable: 'commits',
|
|
746
|
+
foreignColumns: [
|
|
747
|
+
'id'
|
|
748
|
+
],
|
|
749
|
+
name: 'commits_parent_fk'
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
columns: [
|
|
753
|
+
'mergeSourceCommitId'
|
|
754
|
+
],
|
|
755
|
+
foreignTable: 'commits',
|
|
756
|
+
foreignColumns: [
|
|
757
|
+
'id'
|
|
758
|
+
],
|
|
759
|
+
name: 'commits_merge_source_fk'
|
|
760
|
+
}
|
|
761
|
+
],
|
|
762
|
+
indexes: {
|
|
763
|
+
parentIdx: {
|
|
764
|
+
columns: [
|
|
765
|
+
'parentCommitId'
|
|
766
|
+
]
|
|
767
|
+
},
|
|
768
|
+
mergeSourceIdx: {
|
|
769
|
+
columns: [
|
|
770
|
+
'mergeSourceCommitId'
|
|
771
|
+
]
|
|
772
|
+
},
|
|
773
|
+
rootCreatedIdx: {
|
|
774
|
+
columns: [
|
|
775
|
+
'rootId',
|
|
776
|
+
'createdAt'
|
|
777
|
+
]
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
// ========================================================================
|
|
782
|
+
// BRANCHES
|
|
783
|
+
// ========================================================================
|
|
784
|
+
branches: {
|
|
785
|
+
columns: {
|
|
786
|
+
id: {
|
|
787
|
+
type: 'text',
|
|
788
|
+
primaryKey: true,
|
|
789
|
+
defaultId: true,
|
|
790
|
+
defaultIdPrefix: 'branch'
|
|
791
|
+
},
|
|
792
|
+
rootId: {
|
|
793
|
+
type: 'text',
|
|
794
|
+
notNull: true,
|
|
795
|
+
references: {
|
|
796
|
+
table: 'roots',
|
|
797
|
+
column: 'id'
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
name: {
|
|
801
|
+
type: 'text',
|
|
802
|
+
notNull: true
|
|
803
|
+
},
|
|
804
|
+
headCommitId: {
|
|
805
|
+
type: 'text',
|
|
806
|
+
notNull: true,
|
|
807
|
+
references: {
|
|
808
|
+
table: 'commits',
|
|
809
|
+
column: 'id'
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
createdBy: {
|
|
813
|
+
type: 'text'
|
|
814
|
+
},
|
|
815
|
+
createdAt: {
|
|
816
|
+
type: 'timestamp',
|
|
817
|
+
notNull: true,
|
|
818
|
+
defaultNow: true
|
|
819
|
+
},
|
|
820
|
+
updatedAt: {
|
|
821
|
+
type: 'timestamp',
|
|
822
|
+
notNull: true,
|
|
823
|
+
defaultNow: true
|
|
824
|
+
}
|
|
825
|
+
},
|
|
826
|
+
indexes: {
|
|
827
|
+
rootIdIdx: {
|
|
828
|
+
columns: [
|
|
829
|
+
'rootId'
|
|
830
|
+
]
|
|
831
|
+
},
|
|
832
|
+
rootNameUnique: {
|
|
833
|
+
columns: [
|
|
834
|
+
'rootId',
|
|
835
|
+
'name'
|
|
836
|
+
],
|
|
837
|
+
unique: true
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
// ========================================================================
|
|
842
|
+
// BLOCK_VERSIONS
|
|
843
|
+
// ========================================================================
|
|
844
|
+
blockVersions: {
|
|
845
|
+
tableName: 'block_versions',
|
|
846
|
+
indexPrefix: 'bv',
|
|
847
|
+
columns: {
|
|
848
|
+
id: {
|
|
849
|
+
type: 'text',
|
|
850
|
+
primaryKey: true,
|
|
851
|
+
defaultId: true,
|
|
852
|
+
defaultIdPrefix: 'blockVersion'
|
|
853
|
+
},
|
|
854
|
+
blockId: {
|
|
855
|
+
type: 'text',
|
|
856
|
+
notNull: true
|
|
857
|
+
},
|
|
858
|
+
rootId: {
|
|
859
|
+
type: 'text',
|
|
860
|
+
notNull: true,
|
|
861
|
+
references: {
|
|
862
|
+
table: 'roots',
|
|
863
|
+
column: 'id'
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
commitId: {
|
|
867
|
+
type: 'text',
|
|
868
|
+
notNull: true,
|
|
869
|
+
references: {
|
|
870
|
+
table: 'commits',
|
|
871
|
+
column: 'id'
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
type: {
|
|
875
|
+
type: 'text',
|
|
876
|
+
notNull: true
|
|
877
|
+
},
|
|
878
|
+
properties: {
|
|
879
|
+
type: 'jsonb',
|
|
880
|
+
notNull: true,
|
|
881
|
+
jsonType: 'Record<string, unknown>'
|
|
882
|
+
},
|
|
883
|
+
children: {
|
|
884
|
+
type: 'jsonb',
|
|
885
|
+
notNull: true,
|
|
886
|
+
jsonType: 'string[]',
|
|
887
|
+
default: {
|
|
888
|
+
kind: 'literal',
|
|
889
|
+
value: []
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
deleted: {
|
|
893
|
+
type: 'boolean',
|
|
894
|
+
notNull: true,
|
|
895
|
+
default: {
|
|
896
|
+
kind: 'literal',
|
|
897
|
+
value: false
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
createdAt: {
|
|
901
|
+
type: 'timestamp',
|
|
902
|
+
notNull: true,
|
|
903
|
+
defaultNow: true
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
indexes: {
|
|
907
|
+
blockIdIdx: {
|
|
908
|
+
columns: [
|
|
909
|
+
'blockId'
|
|
910
|
+
]
|
|
911
|
+
},
|
|
912
|
+
commitIdIdx: {
|
|
913
|
+
columns: [
|
|
914
|
+
'commitId'
|
|
915
|
+
]
|
|
916
|
+
},
|
|
917
|
+
rootIdIdx: {
|
|
918
|
+
columns: [
|
|
919
|
+
'rootId'
|
|
920
|
+
]
|
|
921
|
+
},
|
|
922
|
+
blockCommitUnique: {
|
|
923
|
+
columns: [
|
|
924
|
+
'blockId',
|
|
925
|
+
'commitId'
|
|
926
|
+
],
|
|
927
|
+
unique: true
|
|
928
|
+
},
|
|
929
|
+
propertiesGin: {
|
|
930
|
+
columns: [
|
|
931
|
+
'properties'
|
|
932
|
+
],
|
|
933
|
+
using: 'gin'
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
},
|
|
937
|
+
// ========================================================================
|
|
938
|
+
// COMMIT_SNAPSHOTS
|
|
939
|
+
// ========================================================================
|
|
940
|
+
commitSnapshots: {
|
|
941
|
+
tableName: 'commit_snapshots',
|
|
942
|
+
indexPrefix: 'cs',
|
|
943
|
+
columns: {
|
|
944
|
+
commitId: {
|
|
945
|
+
type: 'text',
|
|
946
|
+
notNull: true,
|
|
947
|
+
references: {
|
|
948
|
+
table: 'commits',
|
|
949
|
+
column: 'id',
|
|
950
|
+
onDelete: 'cascade'
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
blockId: {
|
|
954
|
+
type: 'text',
|
|
955
|
+
notNull: true
|
|
956
|
+
},
|
|
957
|
+
blockVersionId: {
|
|
958
|
+
type: 'text',
|
|
959
|
+
notNull: true,
|
|
960
|
+
references: {
|
|
961
|
+
table: 'blockVersions',
|
|
962
|
+
column: 'id',
|
|
963
|
+
onDelete: 'cascade'
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
},
|
|
967
|
+
compositePrimaryKey: {
|
|
968
|
+
columns: [
|
|
969
|
+
'commitId',
|
|
970
|
+
'blockId'
|
|
971
|
+
]
|
|
972
|
+
},
|
|
973
|
+
indexes: {
|
|
974
|
+
blockVersionIdx: {
|
|
975
|
+
columns: [
|
|
976
|
+
'blockVersionId'
|
|
977
|
+
]
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
// ========================================================================
|
|
982
|
+
// PUBLICATIONS
|
|
983
|
+
// ========================================================================
|
|
984
|
+
publications: {
|
|
985
|
+
columns: {
|
|
986
|
+
rootId: {
|
|
987
|
+
type: 'text',
|
|
988
|
+
notNull: true,
|
|
989
|
+
references: {
|
|
990
|
+
table: 'roots',
|
|
991
|
+
column: 'id'
|
|
992
|
+
}
|
|
993
|
+
},
|
|
994
|
+
branchId: {
|
|
995
|
+
type: 'text',
|
|
996
|
+
notNull: true,
|
|
997
|
+
references: {
|
|
998
|
+
table: 'branches',
|
|
999
|
+
column: 'id'
|
|
1000
|
+
}
|
|
1001
|
+
},
|
|
1002
|
+
commitId: {
|
|
1003
|
+
type: 'text',
|
|
1004
|
+
notNull: true,
|
|
1005
|
+
references: {
|
|
1006
|
+
table: 'commits',
|
|
1007
|
+
column: 'id'
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
publishedBy: {
|
|
1011
|
+
type: 'text',
|
|
1012
|
+
notNull: true
|
|
1013
|
+
},
|
|
1014
|
+
publishedAt: {
|
|
1015
|
+
type: 'timestamp',
|
|
1016
|
+
notNull: true,
|
|
1017
|
+
defaultNow: true
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
compositePrimaryKey: {
|
|
1021
|
+
columns: [
|
|
1022
|
+
'rootId',
|
|
1023
|
+
'branchId'
|
|
1024
|
+
]
|
|
1025
|
+
},
|
|
1026
|
+
indexes: {
|
|
1027
|
+
branchIdx: {
|
|
1028
|
+
columns: [
|
|
1029
|
+
'branchId'
|
|
1030
|
+
]
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
},
|
|
1034
|
+
// ========================================================================
|
|
1035
|
+
// MERGE_REQUESTS
|
|
1036
|
+
// ========================================================================
|
|
1037
|
+
mergeRequests: {
|
|
1038
|
+
tableName: 'merge_requests',
|
|
1039
|
+
indexPrefix: 'mr',
|
|
1040
|
+
columns: {
|
|
1041
|
+
id: {
|
|
1042
|
+
type: 'text',
|
|
1043
|
+
primaryKey: true,
|
|
1044
|
+
defaultId: true,
|
|
1045
|
+
defaultIdPrefix: 'mergeRequest'
|
|
1046
|
+
},
|
|
1047
|
+
rootId: {
|
|
1048
|
+
type: 'text',
|
|
1049
|
+
notNull: true,
|
|
1050
|
+
references: {
|
|
1051
|
+
table: 'roots',
|
|
1052
|
+
column: 'id'
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
sourceBranchId: {
|
|
1056
|
+
type: 'text',
|
|
1057
|
+
notNull: true,
|
|
1058
|
+
references: {
|
|
1059
|
+
table: 'branches',
|
|
1060
|
+
column: 'id'
|
|
1061
|
+
}
|
|
1062
|
+
},
|
|
1063
|
+
targetBranchId: {
|
|
1064
|
+
type: 'text',
|
|
1065
|
+
notNull: true,
|
|
1066
|
+
references: {
|
|
1067
|
+
table: 'branches',
|
|
1068
|
+
column: 'id'
|
|
1069
|
+
}
|
|
1070
|
+
},
|
|
1071
|
+
sourceCommitId: {
|
|
1072
|
+
type: 'text',
|
|
1073
|
+
notNull: true,
|
|
1074
|
+
references: {
|
|
1075
|
+
table: 'commits',
|
|
1076
|
+
column: 'id'
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
baseCommitId: {
|
|
1080
|
+
type: 'text',
|
|
1081
|
+
references: {
|
|
1082
|
+
table: 'commits',
|
|
1083
|
+
column: 'id'
|
|
1084
|
+
}
|
|
1085
|
+
},
|
|
1086
|
+
mergeCommitId: {
|
|
1087
|
+
type: 'text',
|
|
1088
|
+
references: {
|
|
1089
|
+
table: 'commits',
|
|
1090
|
+
column: 'id'
|
|
1091
|
+
}
|
|
1092
|
+
},
|
|
1093
|
+
status: {
|
|
1094
|
+
type: {
|
|
1095
|
+
enum: 'mergeRequestStatus'
|
|
1096
|
+
},
|
|
1097
|
+
notNull: true,
|
|
1098
|
+
default: {
|
|
1099
|
+
kind: 'literal',
|
|
1100
|
+
value: 'open'
|
|
1101
|
+
}
|
|
1102
|
+
},
|
|
1103
|
+
title: {
|
|
1104
|
+
type: 'text'
|
|
1105
|
+
},
|
|
1106
|
+
description: {
|
|
1107
|
+
type: 'text'
|
|
1108
|
+
},
|
|
1109
|
+
createdBy: {
|
|
1110
|
+
type: 'text',
|
|
1111
|
+
notNull: true
|
|
1112
|
+
},
|
|
1113
|
+
createdAt: {
|
|
1114
|
+
type: 'timestamp',
|
|
1115
|
+
notNull: true,
|
|
1116
|
+
defaultNow: true
|
|
1117
|
+
},
|
|
1118
|
+
updatedAt: {
|
|
1119
|
+
type: 'timestamp',
|
|
1120
|
+
notNull: true,
|
|
1121
|
+
defaultNow: true
|
|
1122
|
+
}
|
|
1123
|
+
},
|
|
1124
|
+
indexes: {
|
|
1125
|
+
rootIdx: {
|
|
1126
|
+
columns: [
|
|
1127
|
+
'rootId'
|
|
1128
|
+
]
|
|
1129
|
+
},
|
|
1130
|
+
sourceBranchIdx: {
|
|
1131
|
+
columns: [
|
|
1132
|
+
'sourceBranchId'
|
|
1133
|
+
]
|
|
1134
|
+
},
|
|
1135
|
+
targetBranchIdx: {
|
|
1136
|
+
columns: [
|
|
1137
|
+
'targetBranchId'
|
|
1138
|
+
]
|
|
1139
|
+
},
|
|
1140
|
+
statusIdx: {
|
|
1141
|
+
columns: [
|
|
1142
|
+
'status'
|
|
1143
|
+
]
|
|
1144
|
+
},
|
|
1145
|
+
openSourceTargetUnique: {
|
|
1146
|
+
columns: [
|
|
1147
|
+
'sourceBranchId',
|
|
1148
|
+
'targetBranchId'
|
|
1149
|
+
],
|
|
1150
|
+
unique: true,
|
|
1151
|
+
where: "status = 'open'"
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
},
|
|
1155
|
+
// ========================================================================
|
|
1156
|
+
// MERGE_CONFLICTS
|
|
1157
|
+
// ========================================================================
|
|
1158
|
+
mergeConflicts: {
|
|
1159
|
+
tableName: 'merge_conflicts',
|
|
1160
|
+
indexPrefix: 'mc',
|
|
1161
|
+
columns: {
|
|
1162
|
+
id: {
|
|
1163
|
+
type: 'text',
|
|
1164
|
+
primaryKey: true,
|
|
1165
|
+
defaultId: true,
|
|
1166
|
+
defaultIdPrefix: 'mergeConflict'
|
|
1167
|
+
},
|
|
1168
|
+
mergeRequestId: {
|
|
1169
|
+
type: 'text',
|
|
1170
|
+
notNull: true,
|
|
1171
|
+
references: {
|
|
1172
|
+
table: 'mergeRequests',
|
|
1173
|
+
column: 'id',
|
|
1174
|
+
onDelete: 'cascade'
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
blockId: {
|
|
1178
|
+
type: 'text',
|
|
1179
|
+
notNull: true
|
|
1180
|
+
},
|
|
1181
|
+
sourceVersionId: {
|
|
1182
|
+
type: 'text',
|
|
1183
|
+
references: {
|
|
1184
|
+
table: 'blockVersions',
|
|
1185
|
+
column: 'id'
|
|
1186
|
+
}
|
|
1187
|
+
},
|
|
1188
|
+
targetVersionId: {
|
|
1189
|
+
type: 'text',
|
|
1190
|
+
references: {
|
|
1191
|
+
table: 'blockVersions',
|
|
1192
|
+
column: 'id'
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
baseVersionId: {
|
|
1196
|
+
type: 'text',
|
|
1197
|
+
references: {
|
|
1198
|
+
table: 'blockVersions',
|
|
1199
|
+
column: 'id'
|
|
1200
|
+
}
|
|
1201
|
+
},
|
|
1202
|
+
resolution: {
|
|
1203
|
+
type: {
|
|
1204
|
+
enum: 'conflictResolution'
|
|
1205
|
+
}
|
|
1206
|
+
},
|
|
1207
|
+
resolvedVersionId: {
|
|
1208
|
+
type: 'text',
|
|
1209
|
+
references: {
|
|
1210
|
+
table: 'blockVersions',
|
|
1211
|
+
column: 'id'
|
|
1212
|
+
}
|
|
1213
|
+
},
|
|
1214
|
+
resolvedBy: {
|
|
1215
|
+
type: 'text'
|
|
1216
|
+
},
|
|
1217
|
+
resolvedAt: {
|
|
1218
|
+
type: 'timestamp'
|
|
1219
|
+
},
|
|
1220
|
+
createdAt: {
|
|
1221
|
+
type: 'timestamp',
|
|
1222
|
+
notNull: true,
|
|
1223
|
+
defaultNow: true
|
|
1224
|
+
}
|
|
1225
|
+
},
|
|
1226
|
+
indexes: {
|
|
1227
|
+
mergeRequestIdx: {
|
|
1228
|
+
columns: [
|
|
1229
|
+
'mergeRequestId'
|
|
1230
|
+
]
|
|
1231
|
+
},
|
|
1232
|
+
mergeBlockUnique: {
|
|
1233
|
+
columns: [
|
|
1234
|
+
'mergeRequestId',
|
|
1235
|
+
'blockId'
|
|
1236
|
+
],
|
|
1237
|
+
unique: true
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
},
|
|
1241
|
+
// ========================================================================
|
|
1242
|
+
// APPROVALS
|
|
1243
|
+
// ========================================================================
|
|
1244
|
+
approvals: {
|
|
1245
|
+
columns: {
|
|
1246
|
+
id: {
|
|
1247
|
+
type: 'text',
|
|
1248
|
+
primaryKey: true,
|
|
1249
|
+
defaultId: true,
|
|
1250
|
+
defaultIdPrefix: 'approval'
|
|
1251
|
+
},
|
|
1252
|
+
mergeRequestId: {
|
|
1253
|
+
type: 'text',
|
|
1254
|
+
references: {
|
|
1255
|
+
table: 'mergeRequests',
|
|
1256
|
+
column: 'id',
|
|
1257
|
+
onDelete: 'cascade'
|
|
1258
|
+
}
|
|
1259
|
+
},
|
|
1260
|
+
branchId: {
|
|
1261
|
+
type: 'text',
|
|
1262
|
+
notNull: true,
|
|
1263
|
+
references: {
|
|
1264
|
+
table: 'branches',
|
|
1265
|
+
column: 'id'
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
commitId: {
|
|
1269
|
+
type: 'text',
|
|
1270
|
+
notNull: true,
|
|
1271
|
+
references: {
|
|
1272
|
+
table: 'commits',
|
|
1273
|
+
column: 'id'
|
|
1274
|
+
}
|
|
1275
|
+
},
|
|
1276
|
+
status: {
|
|
1277
|
+
type: {
|
|
1278
|
+
enum: 'approvalStatus'
|
|
1279
|
+
},
|
|
1280
|
+
notNull: true,
|
|
1281
|
+
default: {
|
|
1282
|
+
kind: 'literal',
|
|
1283
|
+
value: 'pending'
|
|
1284
|
+
}
|
|
1285
|
+
},
|
|
1286
|
+
requestedBy: {
|
|
1287
|
+
type: 'text',
|
|
1288
|
+
notNull: true
|
|
1289
|
+
},
|
|
1290
|
+
requestedReviewer: {
|
|
1291
|
+
type: 'text',
|
|
1292
|
+
notNull: true
|
|
1293
|
+
},
|
|
1294
|
+
reviewedBy: {
|
|
1295
|
+
type: 'text'
|
|
1296
|
+
},
|
|
1297
|
+
message: {
|
|
1298
|
+
type: 'text'
|
|
1299
|
+
},
|
|
1300
|
+
rejectionReason: {
|
|
1301
|
+
type: 'text'
|
|
1302
|
+
},
|
|
1303
|
+
reviewedAt: {
|
|
1304
|
+
type: 'timestamp'
|
|
1305
|
+
},
|
|
1306
|
+
createdAt: {
|
|
1307
|
+
type: 'timestamp',
|
|
1308
|
+
notNull: true,
|
|
1309
|
+
defaultNow: true
|
|
1310
|
+
},
|
|
1311
|
+
updatedAt: {
|
|
1312
|
+
type: 'timestamp',
|
|
1313
|
+
notNull: true,
|
|
1314
|
+
defaultNow: true
|
|
1315
|
+
}
|
|
1316
|
+
},
|
|
1317
|
+
indexes: {
|
|
1318
|
+
mrIdx: {
|
|
1319
|
+
columns: [
|
|
1320
|
+
'mergeRequestId'
|
|
1321
|
+
]
|
|
1322
|
+
},
|
|
1323
|
+
branchIdx: {
|
|
1324
|
+
columns: [
|
|
1325
|
+
'branchId'
|
|
1326
|
+
]
|
|
1327
|
+
},
|
|
1328
|
+
branchCommitIdx: {
|
|
1329
|
+
columns: [
|
|
1330
|
+
'branchId',
|
|
1331
|
+
'commitId'
|
|
1332
|
+
]
|
|
1333
|
+
},
|
|
1334
|
+
statusIdx: {
|
|
1335
|
+
columns: [
|
|
1336
|
+
'status'
|
|
1337
|
+
]
|
|
1338
|
+
},
|
|
1339
|
+
requestedReviewerIdx: {
|
|
1340
|
+
columns: [
|
|
1341
|
+
'requestedReviewer'
|
|
1342
|
+
]
|
|
1343
|
+
},
|
|
1344
|
+
targetReviewerUnique: {
|
|
1345
|
+
columns: [
|
|
1346
|
+
'mergeRequestId',
|
|
1347
|
+
'branchId',
|
|
1348
|
+
'commitId',
|
|
1349
|
+
'requestedReviewer'
|
|
1350
|
+
],
|
|
1351
|
+
unique: true
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
},
|
|
1355
|
+
// ========================================================================
|
|
1356
|
+
// ASSET_FOLDERS
|
|
1357
|
+
// ========================================================================
|
|
1358
|
+
assetFolders: {
|
|
1359
|
+
tableName: 'asset_folders',
|
|
1360
|
+
columns: {
|
|
1361
|
+
id: {
|
|
1362
|
+
type: 'text',
|
|
1363
|
+
primaryKey: true,
|
|
1364
|
+
defaultId: true,
|
|
1365
|
+
defaultIdPrefix: 'assetFolder'
|
|
1366
|
+
},
|
|
1367
|
+
name: {
|
|
1368
|
+
type: 'text',
|
|
1369
|
+
notNull: true
|
|
1370
|
+
},
|
|
1371
|
+
parentId: {
|
|
1372
|
+
type: 'text'
|
|
1373
|
+
},
|
|
1374
|
+
createdBy: {
|
|
1375
|
+
type: 'text'
|
|
1376
|
+
},
|
|
1377
|
+
createdAt: {
|
|
1378
|
+
type: 'timestamp',
|
|
1379
|
+
notNull: true,
|
|
1380
|
+
defaultNow: true
|
|
1381
|
+
}
|
|
1382
|
+
},
|
|
1383
|
+
foreignKeys: [
|
|
1384
|
+
{
|
|
1385
|
+
columns: [
|
|
1386
|
+
'parentId'
|
|
1387
|
+
],
|
|
1388
|
+
foreignTable: 'assetFolders',
|
|
1389
|
+
foreignColumns: [
|
|
1390
|
+
'id'
|
|
1391
|
+
],
|
|
1392
|
+
name: 'asset_folders_parent_fk',
|
|
1393
|
+
onDelete: 'cascade'
|
|
1394
|
+
}
|
|
1395
|
+
],
|
|
1396
|
+
indexes: {
|
|
1397
|
+
parentIdx: {
|
|
1398
|
+
columns: [
|
|
1399
|
+
'parentId'
|
|
1400
|
+
]
|
|
1401
|
+
},
|
|
1402
|
+
nameUnique: {
|
|
1403
|
+
columns: [
|
|
1404
|
+
'parentId',
|
|
1405
|
+
'name'
|
|
1406
|
+
],
|
|
1407
|
+
unique: true
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
},
|
|
1411
|
+
// ========================================================================
|
|
1412
|
+
// ASSETS
|
|
1413
|
+
// ========================================================================
|
|
1414
|
+
assets: {
|
|
1415
|
+
columns: {
|
|
1416
|
+
id: {
|
|
1417
|
+
type: 'text',
|
|
1418
|
+
primaryKey: true,
|
|
1419
|
+
defaultId: true,
|
|
1420
|
+
defaultIdPrefix: 'asset'
|
|
1421
|
+
},
|
|
1422
|
+
slug: {
|
|
1423
|
+
type: 'text',
|
|
1424
|
+
notNull: true
|
|
1425
|
+
},
|
|
1426
|
+
mimeType: {
|
|
1427
|
+
type: 'text',
|
|
1428
|
+
notNull: true
|
|
1429
|
+
},
|
|
1430
|
+
size: {
|
|
1431
|
+
type: 'integer',
|
|
1432
|
+
notNull: true
|
|
1433
|
+
},
|
|
1434
|
+
objectKey: {
|
|
1435
|
+
type: 'text',
|
|
1436
|
+
notNull: true
|
|
1437
|
+
},
|
|
1438
|
+
status: {
|
|
1439
|
+
type: {
|
|
1440
|
+
enum: 'assetStatus'
|
|
1441
|
+
},
|
|
1442
|
+
notNull: true,
|
|
1443
|
+
default: {
|
|
1444
|
+
kind: 'literal',
|
|
1445
|
+
value: 'private'
|
|
1446
|
+
}
|
|
1447
|
+
},
|
|
1448
|
+
folderId: {
|
|
1449
|
+
type: 'text',
|
|
1450
|
+
references: {
|
|
1451
|
+
table: 'assetFolders',
|
|
1452
|
+
column: 'id',
|
|
1453
|
+
onDelete: 'set null'
|
|
1454
|
+
}
|
|
1455
|
+
},
|
|
1456
|
+
variantOf: {
|
|
1457
|
+
type: 'text',
|
|
1458
|
+
references: {
|
|
1459
|
+
table: 'assets',
|
|
1460
|
+
column: 'id',
|
|
1461
|
+
onDelete: 'set null'
|
|
1462
|
+
}
|
|
1463
|
+
},
|
|
1464
|
+
uploadedBy: {
|
|
1465
|
+
type: 'text'
|
|
1466
|
+
},
|
|
1467
|
+
createdAt: {
|
|
1468
|
+
type: 'timestamp',
|
|
1469
|
+
notNull: true,
|
|
1470
|
+
defaultNow: true
|
|
1471
|
+
},
|
|
1472
|
+
updatedAt: {
|
|
1473
|
+
type: 'timestamp',
|
|
1474
|
+
notNull: true,
|
|
1475
|
+
defaultNow: true
|
|
1476
|
+
},
|
|
1477
|
+
archivedAt: {
|
|
1478
|
+
type: 'timestamp'
|
|
1479
|
+
}
|
|
1480
|
+
},
|
|
1481
|
+
indexes: {
|
|
1482
|
+
folderIdx: {
|
|
1483
|
+
columns: [
|
|
1484
|
+
'folderId'
|
|
1485
|
+
]
|
|
1486
|
+
},
|
|
1487
|
+
statusIdx: {
|
|
1488
|
+
columns: [
|
|
1489
|
+
'status'
|
|
1490
|
+
]
|
|
1491
|
+
},
|
|
1492
|
+
variantOfIdx: {
|
|
1493
|
+
columns: [
|
|
1494
|
+
'variantOf'
|
|
1495
|
+
]
|
|
1496
|
+
},
|
|
1497
|
+
objectKeyUnique: {
|
|
1498
|
+
columns: [
|
|
1499
|
+
'objectKey'
|
|
1500
|
+
],
|
|
1501
|
+
unique: true
|
|
1502
|
+
},
|
|
1503
|
+
slugUnique: {
|
|
1504
|
+
columns: [
|
|
1505
|
+
'slug'
|
|
1506
|
+
],
|
|
1507
|
+
unique: true
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
},
|
|
1511
|
+
// ========================================================================
|
|
1512
|
+
// COMMENT_THREADS
|
|
1513
|
+
// ========================================================================
|
|
1514
|
+
commentThreads: {
|
|
1515
|
+
tableName: 'comment_threads',
|
|
1516
|
+
indexPrefix: 'ct',
|
|
1517
|
+
columns: {
|
|
1518
|
+
id: {
|
|
1519
|
+
type: 'text',
|
|
1520
|
+
primaryKey: true,
|
|
1521
|
+
defaultId: true,
|
|
1522
|
+
defaultIdPrefix: 'commentThread'
|
|
1523
|
+
},
|
|
1524
|
+
rootId: {
|
|
1525
|
+
type: 'text',
|
|
1526
|
+
references: {
|
|
1527
|
+
table: 'roots',
|
|
1528
|
+
column: 'id',
|
|
1529
|
+
onDelete: 'cascade'
|
|
1530
|
+
}
|
|
1531
|
+
},
|
|
1532
|
+
collection: {
|
|
1533
|
+
type: 'text',
|
|
1534
|
+
notNull: true
|
|
1535
|
+
},
|
|
1536
|
+
targetType: {
|
|
1537
|
+
type: {
|
|
1538
|
+
enum: 'commentThreadTarget'
|
|
1539
|
+
},
|
|
1540
|
+
notNull: true
|
|
1541
|
+
},
|
|
1542
|
+
mergeRequestId: {
|
|
1543
|
+
type: 'text',
|
|
1544
|
+
references: {
|
|
1545
|
+
table: 'mergeRequests',
|
|
1546
|
+
column: 'id',
|
|
1547
|
+
onDelete: 'cascade'
|
|
1548
|
+
}
|
|
1549
|
+
},
|
|
1550
|
+
blockId: {
|
|
1551
|
+
type: 'text'
|
|
1552
|
+
},
|
|
1553
|
+
commitId: {
|
|
1554
|
+
type: 'text',
|
|
1555
|
+
references: {
|
|
1556
|
+
table: 'commits',
|
|
1557
|
+
column: 'id',
|
|
1558
|
+
onDelete: 'set null'
|
|
1559
|
+
}
|
|
1560
|
+
},
|
|
1561
|
+
status: {
|
|
1562
|
+
type: {
|
|
1563
|
+
enum: 'commentThreadStatus'
|
|
1564
|
+
},
|
|
1565
|
+
notNull: true,
|
|
1566
|
+
default: {
|
|
1567
|
+
kind: 'literal',
|
|
1568
|
+
value: 'open'
|
|
1569
|
+
}
|
|
1570
|
+
},
|
|
1571
|
+
resolvedBy: {
|
|
1572
|
+
type: 'text'
|
|
1573
|
+
},
|
|
1574
|
+
resolvedAt: {
|
|
1575
|
+
type: 'timestamp'
|
|
1576
|
+
},
|
|
1577
|
+
createdBy: {
|
|
1578
|
+
type: 'text',
|
|
1579
|
+
notNull: true
|
|
1580
|
+
},
|
|
1581
|
+
createdAt: {
|
|
1582
|
+
type: 'timestamp',
|
|
1583
|
+
notNull: true,
|
|
1584
|
+
defaultNow: true
|
|
1585
|
+
},
|
|
1586
|
+
updatedAt: {
|
|
1587
|
+
type: 'timestamp',
|
|
1588
|
+
notNull: true,
|
|
1589
|
+
defaultNow: true
|
|
1590
|
+
},
|
|
1591
|
+
deletedAt: {
|
|
1592
|
+
type: 'timestamp'
|
|
1593
|
+
}
|
|
1594
|
+
},
|
|
1595
|
+
indexes: {
|
|
1596
|
+
collectionIdx: {
|
|
1597
|
+
columns: [
|
|
1598
|
+
'collection',
|
|
1599
|
+
'createdAt'
|
|
1600
|
+
]
|
|
1601
|
+
},
|
|
1602
|
+
mrIdx: {
|
|
1603
|
+
columns: [
|
|
1604
|
+
'mergeRequestId',
|
|
1605
|
+
'createdAt'
|
|
1606
|
+
]
|
|
1607
|
+
},
|
|
1608
|
+
blockIdx: {
|
|
1609
|
+
columns: [
|
|
1610
|
+
'blockId',
|
|
1611
|
+
'createdAt'
|
|
1612
|
+
]
|
|
1613
|
+
},
|
|
1614
|
+
commitIdx: {
|
|
1615
|
+
columns: [
|
|
1616
|
+
'commitId',
|
|
1617
|
+
'createdAt'
|
|
1618
|
+
]
|
|
1619
|
+
},
|
|
1620
|
+
rootIdx: {
|
|
1621
|
+
columns: [
|
|
1622
|
+
'rootId',
|
|
1623
|
+
'createdAt'
|
|
1624
|
+
]
|
|
1625
|
+
},
|
|
1626
|
+
statusIdx: {
|
|
1627
|
+
columns: [
|
|
1628
|
+
'status'
|
|
1629
|
+
]
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
},
|
|
1633
|
+
// ========================================================================
|
|
1634
|
+
// COMMENT_MESSAGES
|
|
1635
|
+
// ========================================================================
|
|
1636
|
+
commentMessages: {
|
|
1637
|
+
tableName: 'comment_messages',
|
|
1638
|
+
indexPrefix: 'cm',
|
|
1639
|
+
columns: {
|
|
1640
|
+
id: {
|
|
1641
|
+
type: 'text',
|
|
1642
|
+
primaryKey: true,
|
|
1643
|
+
defaultId: true,
|
|
1644
|
+
defaultIdPrefix: 'commentMessage'
|
|
1645
|
+
},
|
|
1646
|
+
threadId: {
|
|
1647
|
+
type: 'text',
|
|
1648
|
+
notNull: true,
|
|
1649
|
+
references: {
|
|
1650
|
+
table: 'commentThreads',
|
|
1651
|
+
column: 'id',
|
|
1652
|
+
onDelete: 'cascade'
|
|
1653
|
+
}
|
|
1654
|
+
},
|
|
1655
|
+
parentMessageId: {
|
|
1656
|
+
type: 'text'
|
|
1657
|
+
},
|
|
1658
|
+
authorId: {
|
|
1659
|
+
type: 'text'
|
|
1660
|
+
},
|
|
1661
|
+
messageType: {
|
|
1662
|
+
type: {
|
|
1663
|
+
enum: 'commentMessageType'
|
|
1664
|
+
},
|
|
1665
|
+
notNull: true,
|
|
1666
|
+
default: {
|
|
1667
|
+
kind: 'literal',
|
|
1668
|
+
value: 'comment'
|
|
1669
|
+
}
|
|
1670
|
+
},
|
|
1671
|
+
systemType: {
|
|
1672
|
+
type: {
|
|
1673
|
+
enum: 'commentSystemType'
|
|
1674
|
+
}
|
|
1675
|
+
},
|
|
1676
|
+
body: {
|
|
1677
|
+
type: 'text'
|
|
1678
|
+
},
|
|
1679
|
+
meta: {
|
|
1680
|
+
type: 'jsonb',
|
|
1681
|
+
jsonType: 'Record<string, unknown>'
|
|
1682
|
+
},
|
|
1683
|
+
editedAt: {
|
|
1684
|
+
type: 'timestamp'
|
|
1685
|
+
},
|
|
1686
|
+
deletedAt: {
|
|
1687
|
+
type: 'timestamp'
|
|
1688
|
+
},
|
|
1689
|
+
createdAt: {
|
|
1690
|
+
type: 'timestamp',
|
|
1691
|
+
notNull: true,
|
|
1692
|
+
defaultNow: true
|
|
1693
|
+
},
|
|
1694
|
+
updatedAt: {
|
|
1695
|
+
type: 'timestamp',
|
|
1696
|
+
notNull: true,
|
|
1697
|
+
defaultNow: true
|
|
1698
|
+
}
|
|
1699
|
+
},
|
|
1700
|
+
foreignKeys: [
|
|
1701
|
+
{
|
|
1702
|
+
columns: [
|
|
1703
|
+
'parentMessageId'
|
|
1704
|
+
],
|
|
1705
|
+
foreignTable: 'commentMessages',
|
|
1706
|
+
foreignColumns: [
|
|
1707
|
+
'id'
|
|
1708
|
+
],
|
|
1709
|
+
name: 'comment_messages_parent_fk',
|
|
1710
|
+
onDelete: 'set null'
|
|
1711
|
+
}
|
|
1712
|
+
],
|
|
1713
|
+
indexes: {
|
|
1714
|
+
threadIdx: {
|
|
1715
|
+
columns: [
|
|
1716
|
+
'threadId',
|
|
1717
|
+
'createdAt'
|
|
1718
|
+
]
|
|
1719
|
+
},
|
|
1720
|
+
parentIdx: {
|
|
1721
|
+
columns: [
|
|
1722
|
+
'parentMessageId'
|
|
1723
|
+
]
|
|
1724
|
+
},
|
|
1725
|
+
typeIdx: {
|
|
1726
|
+
columns: [
|
|
1727
|
+
'messageType',
|
|
1728
|
+
'systemType'
|
|
1729
|
+
]
|
|
1730
|
+
},
|
|
1731
|
+
authorIdx: {
|
|
1732
|
+
columns: [
|
|
1733
|
+
'authorId',
|
|
1734
|
+
'createdAt'
|
|
1735
|
+
]
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
},
|
|
1739
|
+
// ========================================================================
|
|
1740
|
+
// COMMENT_MENTIONS
|
|
1741
|
+
// ========================================================================
|
|
1742
|
+
commentMentions: {
|
|
1743
|
+
tableName: 'comment_mentions',
|
|
1744
|
+
indexPrefix: 'cmn',
|
|
1745
|
+
columns: {
|
|
1746
|
+
id: {
|
|
1747
|
+
type: 'text',
|
|
1748
|
+
primaryKey: true,
|
|
1749
|
+
defaultId: true,
|
|
1750
|
+
defaultIdPrefix: 'commentMention'
|
|
1751
|
+
},
|
|
1752
|
+
messageId: {
|
|
1753
|
+
type: 'text',
|
|
1754
|
+
notNull: true,
|
|
1755
|
+
references: {
|
|
1756
|
+
table: 'commentMessages',
|
|
1757
|
+
column: 'id',
|
|
1758
|
+
onDelete: 'cascade'
|
|
1759
|
+
}
|
|
1760
|
+
},
|
|
1761
|
+
threadId: {
|
|
1762
|
+
type: 'text',
|
|
1763
|
+
notNull: true,
|
|
1764
|
+
references: {
|
|
1765
|
+
table: 'commentThreads',
|
|
1766
|
+
column: 'id',
|
|
1767
|
+
onDelete: 'cascade'
|
|
1768
|
+
}
|
|
1769
|
+
},
|
|
1770
|
+
mentionedUserId: {
|
|
1771
|
+
type: 'text',
|
|
1772
|
+
notNull: true
|
|
1773
|
+
},
|
|
1774
|
+
mentionedBy: {
|
|
1775
|
+
type: 'text',
|
|
1776
|
+
notNull: true
|
|
1777
|
+
},
|
|
1778
|
+
createdAt: {
|
|
1779
|
+
type: 'timestamp',
|
|
1780
|
+
notNull: true,
|
|
1781
|
+
defaultNow: true
|
|
1782
|
+
}
|
|
1783
|
+
},
|
|
1784
|
+
indexes: {
|
|
1785
|
+
userIdx: {
|
|
1786
|
+
columns: [
|
|
1787
|
+
'mentionedUserId',
|
|
1788
|
+
'createdAt'
|
|
1789
|
+
]
|
|
1790
|
+
},
|
|
1791
|
+
messageIdx: {
|
|
1792
|
+
columns: [
|
|
1793
|
+
'messageId'
|
|
1794
|
+
]
|
|
1795
|
+
},
|
|
1796
|
+
threadUserIdx: {
|
|
1797
|
+
columns: [
|
|
1798
|
+
'threadId',
|
|
1799
|
+
'mentionedUserId'
|
|
1800
|
+
]
|
|
1801
|
+
},
|
|
1802
|
+
messageUserUnique: {
|
|
1803
|
+
columns: [
|
|
1804
|
+
'messageId',
|
|
1805
|
+
'mentionedUserId'
|
|
1806
|
+
],
|
|
1807
|
+
unique: true
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
},
|
|
1811
|
+
// ========================================================================
|
|
1812
|
+
// ASSET_REFERENCES
|
|
1813
|
+
// ========================================================================
|
|
1814
|
+
// ========================================================================
|
|
1815
|
+
// CONTENT_USAGES — One generalist materialized index for ALL content-derived
|
|
1816
|
+
// usage: assets, variables, and reusable-block references. Replaces the old
|
|
1817
|
+
// per-domain asset_references + variable_usages tables (structurally one
|
|
1818
|
+
// pattern). Keyed by the IMMUTABLE blockVersionId: a block diverges per
|
|
1819
|
+
// branch into distinct, append-only versions, so a version-keyed row never
|
|
1820
|
+
// drifts. Rows are inserted ONCE when a version is created (never re-synced)
|
|
1821
|
+
// and removed only by FK cascade when the version (or its root) is pruned.
|
|
1822
|
+
// LIVENESS is decided by joining to branch-HEAD snapshots, never a stored
|
|
1823
|
+
// flag — a superseded version simply stops counting. `targetKey` is
|
|
1824
|
+
// POLYMORPHIC (assetId | variableKey | referencedRootId) and intentionally
|
|
1825
|
+
// carries NO FK: correctness comes from the liveness join, not a per-target
|
|
1826
|
+
// cascade (verified — asset GC reclaim + hardDeleteRoot depend on the
|
|
1827
|
+
// blockVersionId/rootId keys + the join). This is the single source of truth
|
|
1828
|
+
// for the destructive GC/guards AND the usage UIs.
|
|
1829
|
+
contentUsages: {
|
|
1830
|
+
tableName: 'content_usages',
|
|
1831
|
+
indexPrefix: 'cu',
|
|
1832
|
+
columns: {
|
|
1833
|
+
id: {
|
|
1834
|
+
type: 'text',
|
|
1835
|
+
primaryKey: true,
|
|
1836
|
+
defaultId: true,
|
|
1837
|
+
defaultIdPrefix: 'contentUsage'
|
|
1838
|
+
},
|
|
1839
|
+
targetKind: {
|
|
1840
|
+
type: {
|
|
1841
|
+
enum: 'contentUsageTarget'
|
|
1842
|
+
},
|
|
1843
|
+
notNull: true
|
|
1844
|
+
},
|
|
1845
|
+
// assetId | variableKey | referencedRootId — discriminated by targetKind.
|
|
1846
|
+
targetKey: {
|
|
1847
|
+
type: 'text',
|
|
1848
|
+
notNull: true
|
|
1849
|
+
},
|
|
1850
|
+
blockVersionId: {
|
|
1851
|
+
type: 'text',
|
|
1852
|
+
notNull: true,
|
|
1853
|
+
references: {
|
|
1854
|
+
table: 'blockVersions',
|
|
1855
|
+
column: 'id',
|
|
1856
|
+
onDelete: 'cascade'
|
|
1857
|
+
}
|
|
1858
|
+
},
|
|
1859
|
+
// rootId/blockId are the denormalized HOST root/block for fast UI
|
|
1860
|
+
// grouping and for the prune-by-rootId path; rootId also cascades.
|
|
1861
|
+
rootId: {
|
|
1862
|
+
type: 'text',
|
|
1863
|
+
notNull: true,
|
|
1864
|
+
references: {
|
|
1865
|
+
table: 'roots',
|
|
1866
|
+
column: 'id',
|
|
1867
|
+
onDelete: 'cascade'
|
|
1868
|
+
}
|
|
1869
|
+
},
|
|
1870
|
+
blockId: {
|
|
1871
|
+
type: 'text',
|
|
1872
|
+
notNull: true
|
|
1873
|
+
},
|
|
1874
|
+
propertyKey: {
|
|
1875
|
+
type: 'text',
|
|
1876
|
+
notNull: true
|
|
1877
|
+
}
|
|
1878
|
+
},
|
|
1879
|
+
indexes: {
|
|
1880
|
+
versionTargetPropUnique: {
|
|
1881
|
+
columns: [
|
|
1882
|
+
'blockVersionId',
|
|
1883
|
+
'targetKind',
|
|
1884
|
+
'targetKey',
|
|
1885
|
+
'propertyKey'
|
|
1886
|
+
],
|
|
1887
|
+
unique: true
|
|
1888
|
+
},
|
|
1889
|
+
targetIdx: {
|
|
1890
|
+
columns: [
|
|
1891
|
+
'targetKind',
|
|
1892
|
+
'targetKey'
|
|
1893
|
+
]
|
|
1894
|
+
},
|
|
1895
|
+
blockVersionIdx: {
|
|
1896
|
+
columns: [
|
|
1897
|
+
'blockVersionId'
|
|
1898
|
+
]
|
|
1899
|
+
},
|
|
1900
|
+
rootIdx: {
|
|
1901
|
+
columns: [
|
|
1902
|
+
'rootId'
|
|
1903
|
+
]
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
},
|
|
1907
|
+
// ========================================================================
|
|
1908
|
+
// VARIABLES — User-editable key-value pairs for content substitution
|
|
1909
|
+
// ========================================================================
|
|
1910
|
+
variables: {
|
|
1911
|
+
columns: {
|
|
1912
|
+
id: {
|
|
1913
|
+
type: 'text',
|
|
1914
|
+
primaryKey: true,
|
|
1915
|
+
defaultId: true,
|
|
1916
|
+
defaultIdPrefix: 'variable'
|
|
1917
|
+
},
|
|
1918
|
+
key: {
|
|
1919
|
+
type: 'text',
|
|
1920
|
+
notNull: true
|
|
1921
|
+
},
|
|
1922
|
+
value: {
|
|
1923
|
+
type: 'text',
|
|
1924
|
+
notNull: true
|
|
1925
|
+
},
|
|
1926
|
+
description: {
|
|
1927
|
+
type: 'text'
|
|
1928
|
+
},
|
|
1929
|
+
createdBy: {
|
|
1930
|
+
type: 'text'
|
|
1931
|
+
},
|
|
1932
|
+
updatedBy: {
|
|
1933
|
+
type: 'text'
|
|
1934
|
+
},
|
|
1935
|
+
createdAt: {
|
|
1936
|
+
type: 'timestamp',
|
|
1937
|
+
notNull: true,
|
|
1938
|
+
defaultNow: true
|
|
1939
|
+
},
|
|
1940
|
+
updatedAt: {
|
|
1941
|
+
type: 'timestamp',
|
|
1942
|
+
notNull: true,
|
|
1943
|
+
defaultNow: true
|
|
1944
|
+
}
|
|
1945
|
+
},
|
|
1946
|
+
indexes: {
|
|
1947
|
+
keyUnique: {
|
|
1948
|
+
columns: [
|
|
1949
|
+
'key'
|
|
1950
|
+
],
|
|
1951
|
+
unique: true
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
},
|
|
1955
|
+
// ========================================================================
|
|
1956
|
+
// TEMPLATES — Default-value formulas per collection/block/property
|
|
1957
|
+
// ========================================================================
|
|
1958
|
+
templates: {
|
|
1959
|
+
columns: {
|
|
1960
|
+
id: {
|
|
1961
|
+
type: 'text',
|
|
1962
|
+
primaryKey: true,
|
|
1963
|
+
defaultId: true,
|
|
1964
|
+
defaultIdPrefix: 'template'
|
|
1965
|
+
},
|
|
1966
|
+
collection: {
|
|
1967
|
+
type: 'text',
|
|
1968
|
+
notNull: true
|
|
1969
|
+
},
|
|
1970
|
+
blockType: {
|
|
1971
|
+
type: 'text',
|
|
1972
|
+
notNull: true
|
|
1973
|
+
},
|
|
1974
|
+
propertyKey: {
|
|
1975
|
+
type: 'text',
|
|
1976
|
+
notNull: true
|
|
1977
|
+
},
|
|
1978
|
+
template: {
|
|
1979
|
+
type: 'text',
|
|
1980
|
+
notNull: true
|
|
1981
|
+
},
|
|
1982
|
+
description: {
|
|
1983
|
+
type: 'text'
|
|
1984
|
+
},
|
|
1985
|
+
createdBy: {
|
|
1986
|
+
type: 'text'
|
|
1987
|
+
},
|
|
1988
|
+
updatedBy: {
|
|
1989
|
+
type: 'text'
|
|
1990
|
+
},
|
|
1991
|
+
createdAt: {
|
|
1992
|
+
type: 'timestamp',
|
|
1993
|
+
notNull: true,
|
|
1994
|
+
defaultNow: true
|
|
1995
|
+
},
|
|
1996
|
+
updatedAt: {
|
|
1997
|
+
type: 'timestamp',
|
|
1998
|
+
notNull: true,
|
|
1999
|
+
defaultNow: true
|
|
2000
|
+
}
|
|
2001
|
+
},
|
|
2002
|
+
indexes: {
|
|
2003
|
+
collectionBlockPropUnique: {
|
|
2004
|
+
columns: [
|
|
2005
|
+
'collection',
|
|
2006
|
+
'blockType',
|
|
2007
|
+
'propertyKey'
|
|
2008
|
+
],
|
|
2009
|
+
unique: true
|
|
2010
|
+
},
|
|
2011
|
+
collectionIdx: {
|
|
2012
|
+
columns: [
|
|
2013
|
+
'collection'
|
|
2014
|
+
]
|
|
2015
|
+
},
|
|
2016
|
+
collectionBlockIdx: {
|
|
2017
|
+
columns: [
|
|
2018
|
+
'collection',
|
|
2019
|
+
'blockType'
|
|
2020
|
+
]
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
},
|
|
2024
|
+
// ========================================================================
|
|
2025
|
+
// TEMPLATE_VARIABLE_USAGES — Tracks which variables are used in templates
|
|
2026
|
+
// ========================================================================
|
|
2027
|
+
templateVariableUsages: {
|
|
2028
|
+
tableName: 'template_variable_usages',
|
|
2029
|
+
indexPrefix: 'tvu',
|
|
2030
|
+
columns: {
|
|
2031
|
+
id: {
|
|
2032
|
+
type: 'text',
|
|
2033
|
+
primaryKey: true,
|
|
2034
|
+
defaultId: true,
|
|
2035
|
+
defaultIdPrefix: 'tplVarUsage'
|
|
2036
|
+
},
|
|
2037
|
+
variableKey: {
|
|
2038
|
+
type: 'text',
|
|
2039
|
+
notNull: true
|
|
2040
|
+
},
|
|
2041
|
+
templateId: {
|
|
2042
|
+
type: 'text',
|
|
2043
|
+
notNull: true,
|
|
2044
|
+
references: {
|
|
2045
|
+
table: 'templates',
|
|
2046
|
+
column: 'id',
|
|
2047
|
+
onDelete: 'cascade'
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
},
|
|
2051
|
+
indexes: {
|
|
2052
|
+
keyTemplateUnique: {
|
|
2053
|
+
columns: [
|
|
2054
|
+
'variableKey',
|
|
2055
|
+
'templateId'
|
|
2056
|
+
],
|
|
2057
|
+
unique: true
|
|
2058
|
+
},
|
|
2059
|
+
variableKeyIdx: {
|
|
2060
|
+
columns: [
|
|
2061
|
+
'variableKey'
|
|
2062
|
+
]
|
|
2063
|
+
},
|
|
2064
|
+
templateIdIdx: {
|
|
2065
|
+
columns: [
|
|
2066
|
+
'templateId'
|
|
2067
|
+
]
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
},
|
|
2071
|
+
// ========================================================================
|
|
2072
|
+
// SEARCH_INDEX — Materialized full-text search index across all entities
|
|
2073
|
+
// ========================================================================
|
|
2074
|
+
searchIndex: {
|
|
2075
|
+
tableName: 'search_index',
|
|
2076
|
+
indexPrefix: 'si',
|
|
2077
|
+
columns: {
|
|
2078
|
+
id: {
|
|
2079
|
+
type: 'text',
|
|
2080
|
+
primaryKey: true,
|
|
2081
|
+
defaultId: true,
|
|
2082
|
+
defaultIdPrefix: 'si'
|
|
2083
|
+
},
|
|
2084
|
+
entityType: {
|
|
2085
|
+
type: 'text',
|
|
2086
|
+
notNull: true
|
|
2087
|
+
},
|
|
2088
|
+
entityId: {
|
|
2089
|
+
type: 'text',
|
|
2090
|
+
notNull: true
|
|
2091
|
+
},
|
|
2092
|
+
collection: {
|
|
2093
|
+
type: 'text'
|
|
2094
|
+
},
|
|
2095
|
+
rootId: {
|
|
2096
|
+
type: 'text'
|
|
2097
|
+
},
|
|
2098
|
+
contentVector: {
|
|
2099
|
+
type: 'tsvector',
|
|
2100
|
+
notNull: true
|
|
2101
|
+
},
|
|
2102
|
+
title: {
|
|
2103
|
+
type: 'text'
|
|
2104
|
+
},
|
|
2105
|
+
snippet: {
|
|
2106
|
+
type: 'text'
|
|
2107
|
+
},
|
|
2108
|
+
meta: {
|
|
2109
|
+
type: 'jsonb',
|
|
2110
|
+
jsonType: 'Record<string, unknown>'
|
|
2111
|
+
},
|
|
2112
|
+
updatedAt: {
|
|
2113
|
+
type: 'timestamp',
|
|
2114
|
+
notNull: true,
|
|
2115
|
+
defaultNow: true
|
|
2116
|
+
}
|
|
2117
|
+
},
|
|
2118
|
+
indexes: {
|
|
2119
|
+
vectorGin: {
|
|
2120
|
+
columns: [
|
|
2121
|
+
'contentVector'
|
|
2122
|
+
],
|
|
2123
|
+
using: 'gin'
|
|
2124
|
+
},
|
|
2125
|
+
entityTypeIdx: {
|
|
2126
|
+
columns: [
|
|
2127
|
+
'entityType'
|
|
2128
|
+
]
|
|
2129
|
+
},
|
|
2130
|
+
collectionIdx: {
|
|
2131
|
+
columns: [
|
|
2132
|
+
'collection'
|
|
2133
|
+
]
|
|
2134
|
+
},
|
|
2135
|
+
rootIdx: {
|
|
2136
|
+
columns: [
|
|
2137
|
+
'rootId'
|
|
2138
|
+
]
|
|
2139
|
+
},
|
|
2140
|
+
entityUnique: {
|
|
2141
|
+
columns: [
|
|
2142
|
+
'entityType',
|
|
2143
|
+
'entityId'
|
|
2144
|
+
],
|
|
2145
|
+
unique: true
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
},
|
|
2149
|
+
// ========================================================================
|
|
2150
|
+
// NOTIFICATIONS — In-app notification inbox per user
|
|
2151
|
+
// ========================================================================
|
|
2152
|
+
notifications: {
|
|
2153
|
+
tableName: 'notifications',
|
|
2154
|
+
indexPrefix: 'ntf',
|
|
2155
|
+
columns: {
|
|
2156
|
+
id: {
|
|
2157
|
+
type: 'text',
|
|
2158
|
+
primaryKey: true,
|
|
2159
|
+
defaultId: true,
|
|
2160
|
+
defaultIdPrefix: 'notification'
|
|
2161
|
+
},
|
|
2162
|
+
recipientId: {
|
|
2163
|
+
type: 'text',
|
|
2164
|
+
notNull: true
|
|
2165
|
+
},
|
|
2166
|
+
actorId: {
|
|
2167
|
+
type: 'text'
|
|
2168
|
+
},
|
|
2169
|
+
type: {
|
|
2170
|
+
type: {
|
|
2171
|
+
enum: 'notificationType'
|
|
2172
|
+
},
|
|
2173
|
+
notNull: true
|
|
2174
|
+
},
|
|
2175
|
+
title: {
|
|
2176
|
+
type: 'text',
|
|
2177
|
+
notNull: true
|
|
2178
|
+
},
|
|
2179
|
+
body: {
|
|
2180
|
+
type: 'text'
|
|
2181
|
+
},
|
|
2182
|
+
resourceType: {
|
|
2183
|
+
type: 'text'
|
|
2184
|
+
},
|
|
2185
|
+
resourceId: {
|
|
2186
|
+
type: 'text'
|
|
2187
|
+
},
|
|
2188
|
+
collection: {
|
|
2189
|
+
type: 'text'
|
|
2190
|
+
},
|
|
2191
|
+
meta: {
|
|
2192
|
+
type: 'jsonb',
|
|
2193
|
+
jsonType: 'Record<string, unknown>'
|
|
2194
|
+
},
|
|
2195
|
+
readAt: {
|
|
2196
|
+
type: 'timestamp'
|
|
2197
|
+
},
|
|
2198
|
+
archivedAt: {
|
|
2199
|
+
type: 'timestamp'
|
|
2200
|
+
},
|
|
2201
|
+
createdAt: {
|
|
2202
|
+
type: 'timestamp',
|
|
2203
|
+
notNull: true,
|
|
2204
|
+
defaultNow: true
|
|
2205
|
+
}
|
|
2206
|
+
},
|
|
2207
|
+
indexes: {
|
|
2208
|
+
recipientCreatedIdx: {
|
|
2209
|
+
columns: [
|
|
2210
|
+
'recipientId',
|
|
2211
|
+
'createdAt'
|
|
2212
|
+
]
|
|
2213
|
+
},
|
|
2214
|
+
recipientUnreadIdx: {
|
|
2215
|
+
columns: [
|
|
2216
|
+
'recipientId',
|
|
2217
|
+
'readAt'
|
|
2218
|
+
]
|
|
2219
|
+
},
|
|
2220
|
+
resourceIdx: {
|
|
2221
|
+
columns: [
|
|
2222
|
+
'resourceType',
|
|
2223
|
+
'resourceId'
|
|
2224
|
+
]
|
|
2225
|
+
},
|
|
2226
|
+
typeIdx: {
|
|
2227
|
+
columns: [
|
|
2228
|
+
'type'
|
|
2229
|
+
]
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
},
|
|
2233
|
+
// ========================================================================
|
|
2234
|
+
// REDIRECTS — SEO 301/302 mapping (see REDIRECTS_DESIGN.md)
|
|
2235
|
+
// ========================================================================
|
|
2236
|
+
// Source and target are each a page REFERENCE (rootId → current path,
|
|
2237
|
+
// follows moves) or a literal PATH. Auto-created on rename/move/archive,
|
|
2238
|
+
// or managed manually. Resolved by the consumer before serving content.
|
|
2239
|
+
redirects: {
|
|
2240
|
+
tableName: 'redirects',
|
|
2241
|
+
indexPrefix: 'rdr',
|
|
2242
|
+
columns: {
|
|
2243
|
+
id: {
|
|
2244
|
+
type: 'text',
|
|
2245
|
+
primaryKey: true,
|
|
2246
|
+
defaultId: true,
|
|
2247
|
+
defaultIdPrefix: 'redirect'
|
|
2248
|
+
},
|
|
2249
|
+
collection: {
|
|
2250
|
+
type: 'text',
|
|
2251
|
+
notNull: true
|
|
2252
|
+
},
|
|
2253
|
+
sourceType: {
|
|
2254
|
+
type: {
|
|
2255
|
+
enum: 'redirectEndpointType'
|
|
2256
|
+
},
|
|
2257
|
+
notNull: true
|
|
2258
|
+
},
|
|
2259
|
+
// 'page' source: redirect away from this live page (follows its slug).
|
|
2260
|
+
sourceRootId: {
|
|
2261
|
+
type: 'text',
|
|
2262
|
+
references: {
|
|
2263
|
+
table: 'roots',
|
|
2264
|
+
column: 'id',
|
|
2265
|
+
onDelete: 'cascade'
|
|
2266
|
+
}
|
|
2267
|
+
},
|
|
2268
|
+
// 'path' source: the exact (normalized) old/dead path.
|
|
2269
|
+
sourcePath: {
|
|
2270
|
+
type: 'text'
|
|
2271
|
+
},
|
|
2272
|
+
targetType: {
|
|
2273
|
+
type: {
|
|
2274
|
+
enum: 'redirectEndpointType'
|
|
2275
|
+
},
|
|
2276
|
+
notNull: true
|
|
2277
|
+
},
|
|
2278
|
+
// 'page' target: resolves to the root's CURRENT published path.
|
|
2279
|
+
targetRootId: {
|
|
2280
|
+
type: 'text',
|
|
2281
|
+
references: {
|
|
2282
|
+
table: 'roots',
|
|
2283
|
+
column: 'id',
|
|
2284
|
+
onDelete: 'cascade'
|
|
2285
|
+
}
|
|
2286
|
+
},
|
|
2287
|
+
// 'path' target: literal / external destination.
|
|
2288
|
+
targetPath: {
|
|
2289
|
+
type: 'text'
|
|
2290
|
+
},
|
|
2291
|
+
statusCode: {
|
|
2292
|
+
type: 'integer',
|
|
2293
|
+
notNull: true,
|
|
2294
|
+
default: {
|
|
2295
|
+
kind: 'literal',
|
|
2296
|
+
value: 301
|
|
2297
|
+
}
|
|
2298
|
+
},
|
|
2299
|
+
createdBy: {
|
|
2300
|
+
type: 'text'
|
|
2301
|
+
},
|
|
2302
|
+
createdAt: {
|
|
2303
|
+
type: 'timestamp',
|
|
2304
|
+
notNull: true,
|
|
2305
|
+
defaultNow: true
|
|
2306
|
+
},
|
|
2307
|
+
updatedAt: {
|
|
2308
|
+
type: 'timestamp',
|
|
2309
|
+
notNull: true,
|
|
2310
|
+
defaultNow: true
|
|
2311
|
+
},
|
|
2312
|
+
archivedAt: {
|
|
2313
|
+
type: 'timestamp'
|
|
2314
|
+
}
|
|
2315
|
+
},
|
|
2316
|
+
indexes: {
|
|
2317
|
+
// Exact path-source lookup (collection + path). NON-unique on purpose:
|
|
2318
|
+
// uniqueness of a source is enforced at the APPLICATION level
|
|
2319
|
+
// (assertSourceUnique + the auto-create pre-check), scope-filtered where a
|
|
2320
|
+
// scoping plugin is active. A core DB-unique on (collection, sourcePath)
|
|
2321
|
+
// would be GLOBAL — it cannot be loosened by a plugin (merge.ts only ADDS
|
|
2322
|
+
// indexes), so two scopes could never share a path. A scoping
|
|
2323
|
+
// plugin instead adds its own PARTIAL UNIQUE on its scope column(s) + (collection,
|
|
2324
|
+
// sourcePath), which is the real DB guarantee when scoping is on.
|
|
2325
|
+
collectionSourcePathIdx: {
|
|
2326
|
+
columns: [
|
|
2327
|
+
'collection',
|
|
2328
|
+
'sourcePath'
|
|
2329
|
+
]
|
|
2330
|
+
},
|
|
2331
|
+
// Page-source lookup: does this live root redirect away? NON-unique for
|
|
2332
|
+
// the same reason (per-scope uniqueness is the plugin's job).
|
|
2333
|
+
sourceRootIdx: {
|
|
2334
|
+
columns: [
|
|
2335
|
+
'sourceRootId'
|
|
2336
|
+
]
|
|
2337
|
+
},
|
|
2338
|
+
collectionIdx: {
|
|
2339
|
+
columns: [
|
|
2340
|
+
'collection'
|
|
2341
|
+
]
|
|
2342
|
+
},
|
|
2343
|
+
archivedAtIdx: {
|
|
2344
|
+
columns: [
|
|
2345
|
+
'archivedAt'
|
|
2346
|
+
]
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
});
|
|
2352
|
+
|
|
2353
|
+
/** Returns true if a file/path exists and is accessible. */ async function fileExists(filePath) {
|
|
2354
|
+
try {
|
|
2355
|
+
await access(filePath);
|
|
2356
|
+
return true;
|
|
2357
|
+
} catch {
|
|
2358
|
+
return false;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
const CONFIG_CANDIDATES = [
|
|
2363
|
+
'cms.ts',
|
|
2364
|
+
'cms.js',
|
|
2365
|
+
'src/cms.ts',
|
|
2366
|
+
'src/cms.js',
|
|
2367
|
+
'src/lib/cms.ts',
|
|
2368
|
+
'src/lib/cms.js',
|
|
2369
|
+
'lib/cms.ts',
|
|
2370
|
+
'lib/cms.js'
|
|
2371
|
+
];
|
|
2372
|
+
async function discoverConfig(cwd) {
|
|
2373
|
+
for (const candidate of CONFIG_CANDIDATES){
|
|
2374
|
+
const fullPath = path.resolve(cwd, candidate);
|
|
2375
|
+
if (await fileExists(fullPath)) {
|
|
2376
|
+
return fullPath;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
return undefined;
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
const STUB_CODE = `
|
|
2383
|
+
const handler = {
|
|
2384
|
+
get(_, prop) {
|
|
2385
|
+
if (prop === Symbol.toPrimitive) return () => '';
|
|
2386
|
+
if (prop === 'then') return undefined;
|
|
2387
|
+
return stub;
|
|
2388
|
+
},
|
|
2389
|
+
apply() { return stub; },
|
|
2390
|
+
construct() { return stub; },
|
|
2391
|
+
};
|
|
2392
|
+
const stub = new Proxy(function(){}, handler);
|
|
2393
|
+
export default stub;
|
|
2394
|
+
export { stub as auth, stub as db, stub as headers, stub as cookies };
|
|
2395
|
+
`;
|
|
2396
|
+
const SHIM_CODE = `
|
|
2397
|
+
export const createCMS = (definition) => ({
|
|
2398
|
+
...definition,
|
|
2399
|
+
router: {},
|
|
2400
|
+
api: {},
|
|
2401
|
+
collections: definition.collections || {},
|
|
2402
|
+
$plugins: definition.plugins || [],
|
|
2403
|
+
$schema: definition.schema,
|
|
2404
|
+
});
|
|
2405
|
+
// Config authoring helpers are pure identity functions at runtime (see
|
|
2406
|
+
// core/define.ts). The shim re-implements them so a config that imports the
|
|
2407
|
+
// idiomatic \`defineCollection\` / \`defineCollections\` / \`defineAuthMiddleware\`
|
|
2408
|
+
// API loads during \`createcms generate\` without pulling in the real package.
|
|
2409
|
+
export const defineCollection = (collection) => collection;
|
|
2410
|
+
export const defineCollections = (collections) => collections;
|
|
2411
|
+
export const defineAuthMiddleware = (middleware) => middleware;
|
|
2412
|
+
export default { createCMS, defineCollection, defineCollections, defineAuthMiddleware };
|
|
2413
|
+
`;
|
|
2414
|
+
function ensureFile(dir, name, code) {
|
|
2415
|
+
const filePath = path.join(dir, name);
|
|
2416
|
+
try {
|
|
2417
|
+
mkdirSync(dir, {
|
|
2418
|
+
recursive: true
|
|
2419
|
+
});
|
|
2420
|
+
writeFileSync(filePath, code, 'utf8');
|
|
2421
|
+
} catch {
|
|
2422
|
+
// best-effort
|
|
2423
|
+
}
|
|
2424
|
+
return filePath;
|
|
2425
|
+
}
|
|
2426
|
+
function readTsconfigPaths(cwd) {
|
|
2427
|
+
const alias = {};
|
|
2428
|
+
try {
|
|
2429
|
+
const raw = readFileSync(path.join(cwd, 'tsconfig.json'), 'utf8');
|
|
2430
|
+
const stripped = raw.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
|
|
2431
|
+
const tsconfig = JSON.parse(stripped);
|
|
2432
|
+
const paths = tsconfig.compilerOptions?.paths;
|
|
2433
|
+
const baseUrl = tsconfig.compilerOptions?.baseUrl ?? '.';
|
|
2434
|
+
const base = path.resolve(cwd, baseUrl);
|
|
2435
|
+
if (paths) {
|
|
2436
|
+
for (const [pattern, targets] of Object.entries(paths)){
|
|
2437
|
+
if (targets.length > 0) {
|
|
2438
|
+
const key = pattern.replace(/\/\*$/, '');
|
|
2439
|
+
const target = targets[0].replace(/\/\*$/, '');
|
|
2440
|
+
alias[key] = path.resolve(base, target);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
} catch {
|
|
2445
|
+
// No tsconfig or invalid — skip
|
|
2446
|
+
}
|
|
2447
|
+
return alias;
|
|
2448
|
+
}
|
|
2449
|
+
/**
|
|
2450
|
+
* Scans node_modules directories upward from cwd and collects all
|
|
2451
|
+
* installed package names. These will be aliased to the stub so
|
|
2452
|
+
* jiti never loads them (preventing side effects).
|
|
2453
|
+
*/ function collectInstalledPackages(cwd) {
|
|
2454
|
+
const packages = new Set();
|
|
2455
|
+
let dir = cwd;
|
|
2456
|
+
while(true){
|
|
2457
|
+
const nmDir = path.join(dir, 'node_modules');
|
|
2458
|
+
try {
|
|
2459
|
+
for (const entry of readdirSync(nmDir)){
|
|
2460
|
+
if (entry.startsWith('.')) continue;
|
|
2461
|
+
if (entry.startsWith('@')) {
|
|
2462
|
+
const scopeDir = path.join(nmDir, entry);
|
|
2463
|
+
try {
|
|
2464
|
+
for (const pkg of readdirSync(scopeDir)){
|
|
2465
|
+
if (!pkg.startsWith('.')) packages.add(`${entry}/${pkg}`);
|
|
2466
|
+
}
|
|
2467
|
+
} catch {
|
|
2468
|
+
// not readable
|
|
2469
|
+
}
|
|
2470
|
+
} else {
|
|
2471
|
+
packages.add(entry);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
} catch {
|
|
2475
|
+
// no node_modules here
|
|
2476
|
+
}
|
|
2477
|
+
const parent = path.dirname(dir);
|
|
2478
|
+
if (parent === dir) break;
|
|
2479
|
+
dir = parent;
|
|
2480
|
+
}
|
|
2481
|
+
return [
|
|
2482
|
+
...packages
|
|
2483
|
+
];
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* CJS fallback patch: catches any require() that still slips through
|
|
2487
|
+
* jiti's alias system and redirects to the stub.
|
|
2488
|
+
*/ function patchModuleResolution(stubPath) {
|
|
2489
|
+
const original = Module._resolveFilename;
|
|
2490
|
+
Module._resolveFilename = function(request, parent, isMain, options) {
|
|
2491
|
+
try {
|
|
2492
|
+
return original.call(this, request, parent, isMain, options);
|
|
2493
|
+
} catch {
|
|
2494
|
+
return stubPath;
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
return ()=>{
|
|
2498
|
+
Module._resolveFilename = original;
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Resolves the real @createcms/core dist directory by locating one of
|
|
2503
|
+
* its exported subpaths and walking up to the dist root.
|
|
2504
|
+
*/ function resolveRealCmsDistDir(cwd) {
|
|
2505
|
+
try {
|
|
2506
|
+
const req = Module.createRequire(path.join(cwd, '_'));
|
|
2507
|
+
// Resolve a known export — ./nanoid maps to dist/nanoid.js
|
|
2508
|
+
const nanoidPath = req.resolve('@createcms/core/nanoid');
|
|
2509
|
+
// nanoidPath = <pkg>/dist/nanoid.js → dirname = <pkg>/dist
|
|
2510
|
+
return path.dirname(nanoidPath);
|
|
2511
|
+
} catch {
|
|
2512
|
+
return null;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
async function loadCMSConfig(configPath) {
|
|
2516
|
+
const cwd = path.dirname(configPath);
|
|
2517
|
+
const tsconfigAlias = readTsconfigPaths(cwd);
|
|
2518
|
+
const tmpDir = path.join(tmpdir(), 'createcms-stubs');
|
|
2519
|
+
const stubPath = ensureFile(tmpDir, 'module-stub.mjs', STUB_CODE);
|
|
2520
|
+
const shimPath = ensureFile(tmpDir, 'cms-shim.mjs', SHIM_CODE);
|
|
2521
|
+
// Build alias map: stub every installed package, then override
|
|
2522
|
+
// @createcms/core with the lightweight shim
|
|
2523
|
+
const allPackages = collectInstalledPackages(cwd);
|
|
2524
|
+
const alias = {};
|
|
2525
|
+
for (const pkg of allPackages){
|
|
2526
|
+
alias[pkg] = stubPath;
|
|
2527
|
+
}
|
|
2528
|
+
alias['@createcms/core'] = shimPath;
|
|
2529
|
+
// Jiti treats aliases as prefix replacements, so @createcms/core/plugins/server
|
|
2530
|
+
// would resolve to <shimPath>/plugins/server (which doesn't exist).
|
|
2531
|
+
// We need explicit aliases for subpaths that must resolve to the real package
|
|
2532
|
+
// so the generator can load plugin schemas.
|
|
2533
|
+
const cmsDistDir = resolveRealCmsDistDir(cwd);
|
|
2534
|
+
if (cmsDistDir) {
|
|
2535
|
+
for (const key of Object.keys(alias)){
|
|
2536
|
+
if (key.startsWith('@createcms/core/')) {
|
|
2537
|
+
delete alias[key];
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
// Map import specifiers → dist entry points
|
|
2541
|
+
const subpathMap = {
|
|
2542
|
+
'plugins/server': 'plugins/server.js',
|
|
2543
|
+
'plugins/client': 'plugins/client.js',
|
|
2544
|
+
'plugins/multi-tenant': 'plugins/multi-tenant/index.js',
|
|
2545
|
+
'plugins/ab-test': 'plugins/ab-test/index.js',
|
|
2546
|
+
'plugins/ab-test/client': 'plugins/ab-test/client.js',
|
|
2547
|
+
'plugins/media-optimize': 'plugins/media-optimize/index.js',
|
|
2548
|
+
plugins: 'plugins/index.js',
|
|
2549
|
+
nanoid: 'nanoid.js'
|
|
2550
|
+
};
|
|
2551
|
+
for (const [specifier, distRelative] of Object.entries(subpathMap)){
|
|
2552
|
+
alias[`@createcms/core/${specifier}`] = path.join(cmsDistDir, distRelative);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
// tsconfig paths override everything (e.g. @/* → ./src/*)
|
|
2556
|
+
Object.assign(alias, tsconfigAlias);
|
|
2557
|
+
const restore = patchModuleResolution(stubPath);
|
|
2558
|
+
try {
|
|
2559
|
+
const jiti = createJiti(pathToFileURL(configPath).href, {
|
|
2560
|
+
interopDefault: true,
|
|
2561
|
+
moduleCache: false,
|
|
2562
|
+
alias
|
|
2563
|
+
});
|
|
2564
|
+
const mod = await jiti.import(configPath);
|
|
2565
|
+
// jiti with interopDefault may nest the exports — try multiple paths
|
|
2566
|
+
const instance = mod.cms ?? mod.default ?? mod;
|
|
2567
|
+
if (!instance || typeof instance !== 'object') {
|
|
2568
|
+
throw new Error(`No CMS instance found. Export your createCMS() result as default or named "cms" from ${configPath}`);
|
|
2569
|
+
}
|
|
2570
|
+
return instance;
|
|
2571
|
+
} finally{
|
|
2572
|
+
restore();
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
function printMeta(label, value) {
|
|
2577
|
+
console.log(` ${kleur.bold().gray(label)} ${value}`);
|
|
2578
|
+
}
|
|
2579
|
+
function createSpinner(text) {
|
|
2580
|
+
return ora({
|
|
2581
|
+
text: ` ${text}`,
|
|
2582
|
+
color: 'cyan'
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
async function confirmOverwrite(filePath) {
|
|
2586
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2587
|
+
console.error(`\n ${kleur.red('Error:')} Output file already exists and no interactive terminal is available.`);
|
|
2588
|
+
printMeta('file', filePath);
|
|
2589
|
+
printMeta('hint', 'Delete the file first or run the command in an interactive terminal.');
|
|
2590
|
+
return false;
|
|
2591
|
+
}
|
|
2592
|
+
console.log();
|
|
2593
|
+
console.log(` ${kleur.yellow('!')} ${kleur.bold('Output file already exists.')}`);
|
|
2594
|
+
printMeta('file', filePath);
|
|
2595
|
+
const response = await prompts({
|
|
2596
|
+
type: 'confirm',
|
|
2597
|
+
name: 'overwrite',
|
|
2598
|
+
message: 'Overwrite existing file?',
|
|
2599
|
+
initial: false
|
|
2600
|
+
}, {
|
|
2601
|
+
onCancel: ()=>false
|
|
2602
|
+
});
|
|
2603
|
+
return response.overwrite === true;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
function collectSchemaSources(config) {
|
|
2607
|
+
const sources = [
|
|
2608
|
+
{
|
|
2609
|
+
name: 'core',
|
|
2610
|
+
schema: coreSchema
|
|
2611
|
+
}
|
|
2612
|
+
];
|
|
2613
|
+
if (config.$plugins) {
|
|
2614
|
+
for (const plugin of config.$plugins){
|
|
2615
|
+
if (plugin.schema) {
|
|
2616
|
+
sources.push({
|
|
2617
|
+
name: `plugin:${plugin.id}`,
|
|
2618
|
+
schema: plugin.schema
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
return sources;
|
|
2624
|
+
}
|
|
2625
|
+
function registerGenerateCommand(cli) {
|
|
2626
|
+
cli.command('generate [config]', 'Generate the Drizzle schema from your CMS config').option('--output <path>', 'Override the output file path').action(async (configArg, options)=>{
|
|
2627
|
+
const cwd = process.cwd();
|
|
2628
|
+
const spinner = createSpinner('Looking for config');
|
|
2629
|
+
spinner.start();
|
|
2630
|
+
let configPath;
|
|
2631
|
+
if (configArg) {
|
|
2632
|
+
configPath = path.resolve(cwd, configArg);
|
|
2633
|
+
} else {
|
|
2634
|
+
const discovered = await discoverConfig(cwd);
|
|
2635
|
+
if (!discovered) {
|
|
2636
|
+
spinner.fail('No cms.ts found. Searched cms.ts, src/cms.ts, src/lib/cms.ts. Pass a path: createcms generate ./path/to/cms.ts');
|
|
2637
|
+
process.exit(1);
|
|
2638
|
+
}
|
|
2639
|
+
configPath = discovered;
|
|
2640
|
+
}
|
|
2641
|
+
spinner.succeed(` Found: ${configPath}`);
|
|
2642
|
+
const loadSpinner = createSpinner('Loading CMS config');
|
|
2643
|
+
loadSpinner.start();
|
|
2644
|
+
let config;
|
|
2645
|
+
try {
|
|
2646
|
+
config = await loadCMSConfig(configPath);
|
|
2647
|
+
} catch (err) {
|
|
2648
|
+
loadSpinner.fail(' Failed to load CMS config');
|
|
2649
|
+
throw err;
|
|
2650
|
+
}
|
|
2651
|
+
loadSpinner.succeed(' Config loaded');
|
|
2652
|
+
const outputPath = path.resolve(cwd, options?.output ?? config.$schema?.output ?? './cms-schema.ts');
|
|
2653
|
+
console.log();
|
|
2654
|
+
printMeta('config', kleur.dim(configPath));
|
|
2655
|
+
printMeta('output', kleur.dim(outputPath));
|
|
2656
|
+
const sources = collectSchemaSources(config);
|
|
2657
|
+
const pluginCount = sources.length - 1;
|
|
2658
|
+
if (pluginCount > 0) {
|
|
2659
|
+
printMeta('plugins', kleur.dim(sources.filter((s)=>s.name !== 'core').map((s)=>s.name.replace('plugin:', '')).join(', ')));
|
|
2660
|
+
}
|
|
2661
|
+
if (await fileExists(outputPath)) {
|
|
2662
|
+
const shouldOverwrite = await confirmOverwrite(outputPath);
|
|
2663
|
+
if (!shouldOverwrite) {
|
|
2664
|
+
console.log(`\n ${kleur.yellow('Generation cancelled.')}`);
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
const genSpinner = createSpinner('Generating schema');
|
|
2669
|
+
genSpinner.start();
|
|
2670
|
+
try {
|
|
2671
|
+
const result = await generateSchema({
|
|
2672
|
+
sources,
|
|
2673
|
+
outputPath
|
|
2674
|
+
});
|
|
2675
|
+
genSpinner.succeed(' Schema generated');
|
|
2676
|
+
console.log();
|
|
2677
|
+
printMeta('file', kleur.green(result.outputPath));
|
|
2678
|
+
printMeta('tables', kleur.green(String(Object.keys(result.schema.tables).length)));
|
|
2679
|
+
printMeta('enums', kleur.green(String(Object.keys(result.schema.enums).length)));
|
|
2680
|
+
console.log();
|
|
2681
|
+
} catch (error) {
|
|
2682
|
+
genSpinner.fail(' Schema generation failed');
|
|
2683
|
+
throw error;
|
|
2684
|
+
}
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
// ============================================================================
|
|
2689
|
+
// `createcms init` scaffold templates + collection presets
|
|
2690
|
+
// ============================================================================
|
|
2691
|
+
//
|
|
2692
|
+
// Plain string templates (bundled into dist by bunchee — NOT read from disk at
|
|
2693
|
+
// runtime). They target a Next.js App Router project using the create-next-app
|
|
2694
|
+
// `--src-dir` layout (files under `src/`, the `@/*` → `./src/*` path alias).
|
|
2695
|
+
// db wiring is intentionally left to the consumer (`@/lib/db`) — `init` only
|
|
2696
|
+
// scaffolds the CMS side.
|
|
2697
|
+
/**
|
|
2698
|
+
* A ready-made collection the consumer can scaffold via `createcms init
|
|
2699
|
+
* --preset <name>`. The scaffolded collection file is editable code the
|
|
2700
|
+
* consumer OWNS (presets are a starting point, not a maintained import).
|
|
2701
|
+
*/ const pagesCollection = ()=>`import type { CollectionDefinition } from '@createcms/core';
|
|
2702
|
+
|
|
2703
|
+
export const pagesCollection = {
|
|
2704
|
+
label: 'Pages',
|
|
2705
|
+
description: 'Marketing and content pages.',
|
|
2706
|
+
slug: { enabled: true, root: '/' },
|
|
2707
|
+
root: {
|
|
2708
|
+
properties: {
|
|
2709
|
+
title: {
|
|
2710
|
+
type: 'string',
|
|
2711
|
+
label: 'Title',
|
|
2712
|
+
required: true,
|
|
2713
|
+
defaultValue: 'Untitled page',
|
|
2714
|
+
},
|
|
2715
|
+
},
|
|
2716
|
+
},
|
|
2717
|
+
blocks: {
|
|
2718
|
+
richText: {
|
|
2719
|
+
label: 'Rich Text',
|
|
2720
|
+
description: 'A block of formatted text.',
|
|
2721
|
+
properties: {
|
|
2722
|
+
content: { type: 'richText', label: 'Content', required: true },
|
|
2723
|
+
},
|
|
2724
|
+
},
|
|
2725
|
+
},
|
|
2726
|
+
} as const satisfies CollectionDefinition;
|
|
2727
|
+
|
|
2728
|
+
export type PagesCollection = typeof pagesCollection;
|
|
2729
|
+
`;
|
|
2730
|
+
const postsCollection = ()=>`import type { CollectionDefinition } from '@createcms/core';
|
|
2731
|
+
|
|
2732
|
+
export const postsCollection = {
|
|
2733
|
+
label: 'Blog Posts',
|
|
2734
|
+
description: 'Articles and updates.',
|
|
2735
|
+
slug: { enabled: true, root: '/blog' },
|
|
2736
|
+
root: {
|
|
2737
|
+
properties: {
|
|
2738
|
+
title: {
|
|
2739
|
+
type: 'string',
|
|
2740
|
+
label: 'Title',
|
|
2741
|
+
required: true,
|
|
2742
|
+
defaultValue: 'Untitled post',
|
|
2743
|
+
},
|
|
2744
|
+
excerpt: {
|
|
2745
|
+
type: 'string',
|
|
2746
|
+
label: 'Excerpt',
|
|
2747
|
+
placeholder: 'A short summary shown in listings.',
|
|
2748
|
+
},
|
|
2749
|
+
publishedAt: { type: 'date', label: 'Published at' },
|
|
2750
|
+
draft: { type: 'boolean', label: 'Draft', defaultValue: false },
|
|
2751
|
+
},
|
|
2752
|
+
},
|
|
2753
|
+
blocks: {
|
|
2754
|
+
richText: {
|
|
2755
|
+
label: 'Rich Text',
|
|
2756
|
+
description: 'A block of formatted text.',
|
|
2757
|
+
properties: {
|
|
2758
|
+
content: { type: 'richText', label: 'Content', required: true },
|
|
2759
|
+
},
|
|
2760
|
+
},
|
|
2761
|
+
image: {
|
|
2762
|
+
label: 'Image',
|
|
2763
|
+
description: 'A figure with optional caption.',
|
|
2764
|
+
properties: {
|
|
2765
|
+
src: { type: 'image', label: 'Image', required: true },
|
|
2766
|
+
alt: { type: 'string', label: 'Alt text', required: true },
|
|
2767
|
+
caption: { type: 'string', label: 'Caption' },
|
|
2768
|
+
},
|
|
2769
|
+
},
|
|
2770
|
+
},
|
|
2771
|
+
} as const satisfies CollectionDefinition;
|
|
2772
|
+
|
|
2773
|
+
export type PostsCollection = typeof postsCollection;
|
|
2774
|
+
`;
|
|
2775
|
+
const docsCollection = ()=>`import type { CollectionDefinition } from '@createcms/core';
|
|
2776
|
+
|
|
2777
|
+
export const docsCollection = {
|
|
2778
|
+
label: 'Docs',
|
|
2779
|
+
description: 'Nested documentation pages.',
|
|
2780
|
+
slug: { enabled: true, root: '/docs', nested: true, normalize: true },
|
|
2781
|
+
root: {
|
|
2782
|
+
properties: {
|
|
2783
|
+
title: {
|
|
2784
|
+
type: 'string',
|
|
2785
|
+
label: 'Title',
|
|
2786
|
+
required: true,
|
|
2787
|
+
defaultValue: 'Untitled page',
|
|
2788
|
+
},
|
|
2789
|
+
},
|
|
2790
|
+
},
|
|
2791
|
+
blocks: {
|
|
2792
|
+
richText: {
|
|
2793
|
+
label: 'Rich Text',
|
|
2794
|
+
description: 'A block of formatted text.',
|
|
2795
|
+
properties: {
|
|
2796
|
+
content: { type: 'richText', label: 'Content', required: true },
|
|
2797
|
+
},
|
|
2798
|
+
},
|
|
2799
|
+
callout: {
|
|
2800
|
+
label: 'Callout',
|
|
2801
|
+
description: 'A highlighted note.',
|
|
2802
|
+
properties: {
|
|
2803
|
+
variant: {
|
|
2804
|
+
type: 'select',
|
|
2805
|
+
label: 'Variant',
|
|
2806
|
+
defaultValue: 'info',
|
|
2807
|
+
options: [
|
|
2808
|
+
{ label: 'Info', value: 'info' },
|
|
2809
|
+
{ label: 'Warning', value: 'warning' },
|
|
2810
|
+
{ label: 'Tip', value: 'tip' },
|
|
2811
|
+
],
|
|
2812
|
+
},
|
|
2813
|
+
content: { type: 'richText', label: 'Content', required: true },
|
|
2814
|
+
},
|
|
2815
|
+
},
|
|
2816
|
+
},
|
|
2817
|
+
} as const satisfies CollectionDefinition;
|
|
2818
|
+
|
|
2819
|
+
export type DocsCollection = typeof docsCollection;
|
|
2820
|
+
`;
|
|
2821
|
+
/** The shipped presets, keyed by `--preset <name>`. */ const PRESETS = {
|
|
2822
|
+
pages: {
|
|
2823
|
+
name: 'pages',
|
|
2824
|
+
description: 'Marketing & content pages (the default).',
|
|
2825
|
+
fileName: 'pages',
|
|
2826
|
+
exportName: 'pagesCollection',
|
|
2827
|
+
collectionKey: 'pages',
|
|
2828
|
+
collection: pagesCollection
|
|
2829
|
+
},
|
|
2830
|
+
blog: {
|
|
2831
|
+
name: 'blog',
|
|
2832
|
+
description: 'A blog: posts with excerpt, date, cover image + rich text.',
|
|
2833
|
+
fileName: 'posts',
|
|
2834
|
+
exportName: 'postsCollection',
|
|
2835
|
+
collectionKey: 'posts',
|
|
2836
|
+
collection: postsCollection
|
|
2837
|
+
},
|
|
2838
|
+
docs: {
|
|
2839
|
+
name: 'docs',
|
|
2840
|
+
description: 'Nested documentation pages with callouts.',
|
|
2841
|
+
fileName: 'docs',
|
|
2842
|
+
exportName: 'docsCollection',
|
|
2843
|
+
collectionKey: 'docs',
|
|
2844
|
+
collection: docsCollection
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
const DEFAULT_PRESET = 'pages';
|
|
2848
|
+
/** `src/lib/cms.ts` — the createCMS config, wired to the chosen preset. */ const cmsConfigTemplate = (preset)=>`import { createCMS } from '@createcms/core';
|
|
2849
|
+
|
|
2850
|
+
import { ${preset.exportName} } from '@/cms/collections/${preset.fileName}';
|
|
2851
|
+
// TODO: point this at YOUR Drizzle client (a DrizzleInstance). createcms does
|
|
2852
|
+
// not scaffold the database client — wire your own (see https://orm.drizzle.team).
|
|
2853
|
+
import { db } from '@/lib/db';
|
|
2854
|
+
|
|
2855
|
+
export const cms = createCMS({
|
|
2856
|
+
db,
|
|
2857
|
+
// Where \`createcms generate\` writes the Drizzle schema for the CMS tables.
|
|
2858
|
+
schema: {
|
|
2859
|
+
output: './src/db/schema/cms.ts',
|
|
2860
|
+
},
|
|
2861
|
+
collections: {
|
|
2862
|
+
${preset.collectionKey}: ${preset.exportName},
|
|
2863
|
+
},
|
|
2864
|
+
// Media uploads target any S3-compatible bucket. Fill from your environment.
|
|
2865
|
+
media: {
|
|
2866
|
+
provider: 'aws',
|
|
2867
|
+
region: process.env.S3_REGION!,
|
|
2868
|
+
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
|
|
2869
|
+
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
|
|
2870
|
+
bucketName: process.env.S3_BUCKET!,
|
|
2871
|
+
publicUrl: process.env.S3_PUBLIC_URL!,
|
|
2872
|
+
},
|
|
2873
|
+
// Published content is public; everything else requires auth. Replace the
|
|
2874
|
+
// TODO with your real session/permission check (return {} to allow, throw to
|
|
2875
|
+
// deny).
|
|
2876
|
+
authMiddleware: async (ctx) => {
|
|
2877
|
+
if (ctx.permissionResource === 'publishedContent') return {};
|
|
2878
|
+
// TODO: resolve the signed-in user / permissions here.
|
|
2879
|
+
throw new Error('Unauthorized');
|
|
2880
|
+
},
|
|
2881
|
+
});
|
|
2882
|
+
`;
|
|
2883
|
+
/** `src/app/api/cms/[[...rest]]/route.ts` — mounts the CMS HTTP router. */ const routeHandlerTemplate = ()=>`import { cms } from '@/lib/cms';
|
|
2884
|
+
|
|
2885
|
+
const { handler } = cms.router;
|
|
2886
|
+
|
|
2887
|
+
export const GET = handler;
|
|
2888
|
+
export const POST = handler;
|
|
2889
|
+
`;
|
|
2890
|
+
/** `.env.example` — the env vars the scaffolded config reads. */ const envExampleTemplate = ()=>`# The CMS database — any Postgres connection string.
|
|
2891
|
+
DATABASE_URL=
|
|
2892
|
+
|
|
2893
|
+
# Media uploads — any S3-compatible bucket.
|
|
2894
|
+
S3_REGION=
|
|
2895
|
+
S3_ACCESS_KEY_ID=
|
|
2896
|
+
S3_SECRET_ACCESS_KEY=
|
|
2897
|
+
S3_BUCKET=
|
|
2898
|
+
S3_PUBLIC_URL=
|
|
2899
|
+
`;
|
|
2900
|
+
/** The npm script `init` adds to package.json. */ const GENERATE_SCRIPT = {
|
|
2901
|
+
name: 'cms:generate',
|
|
2902
|
+
command: 'createcms generate'
|
|
2903
|
+
};
|
|
2904
|
+
/**
|
|
2905
|
+
* The files `init` scaffolds for a chosen preset, relative to the project root.
|
|
2906
|
+
* Order is display order. Paths assume the `src/` layout (module header).
|
|
2907
|
+
*/ function buildInitFiles(preset) {
|
|
2908
|
+
return [
|
|
2909
|
+
{
|
|
2910
|
+
path: 'src/lib/cms.ts',
|
|
2911
|
+
content: ()=>cmsConfigTemplate(preset)
|
|
2912
|
+
},
|
|
2913
|
+
{
|
|
2914
|
+
path: `src/cms/collections/${preset.fileName}.ts`,
|
|
2915
|
+
content: preset.collection
|
|
2916
|
+
},
|
|
2917
|
+
{
|
|
2918
|
+
path: 'src/app/api/cms/[[...rest]]/route.ts',
|
|
2919
|
+
content: routeHandlerTemplate
|
|
2920
|
+
},
|
|
2921
|
+
{
|
|
2922
|
+
path: '.env.example',
|
|
2923
|
+
content: envExampleTemplate
|
|
2924
|
+
}
|
|
2925
|
+
];
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
/**
|
|
2929
|
+
* Resolve which preset to scaffold. An explicit `--preset` wins (and a typo is
|
|
2930
|
+
* a hard error listing the valid names). Otherwise an interactive picker runs
|
|
2931
|
+
* when a TTY is available; non-interactively it falls back to the default.
|
|
2932
|
+
*/ async function resolvePreset(flag) {
|
|
2933
|
+
if (flag) {
|
|
2934
|
+
// Object.hasOwn (not `in`) so prototype keys (constructor, __proto__, …) are
|
|
2935
|
+
// rejected as unknown instead of resolving to a bogus non-preset value.
|
|
2936
|
+
if (!Object.hasOwn(PRESETS, flag)) {
|
|
2937
|
+
throw new Error(`Unknown preset "${flag}". Available: ${Object.keys(PRESETS).join(', ')}.`);
|
|
2938
|
+
}
|
|
2939
|
+
return PRESETS[flag];
|
|
2940
|
+
}
|
|
2941
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
2942
|
+
const response = await prompts({
|
|
2943
|
+
type: 'select',
|
|
2944
|
+
name: 'preset',
|
|
2945
|
+
message: 'Which collection preset?',
|
|
2946
|
+
choices: Object.values(PRESETS).map((p)=>({
|
|
2947
|
+
title: p.name,
|
|
2948
|
+
description: p.description,
|
|
2949
|
+
value: p.name
|
|
2950
|
+
})),
|
|
2951
|
+
initial: Object.keys(PRESETS).indexOf(DEFAULT_PRESET)
|
|
2952
|
+
});
|
|
2953
|
+
// Ctrl+C / escape leaves preset undefined → fall through to the default.
|
|
2954
|
+
if (typeof response.preset === 'string' && Object.hasOwn(PRESETS, response.preset)) {
|
|
2955
|
+
return PRESETS[response.preset];
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
return PRESETS[DEFAULT_PRESET];
|
|
2959
|
+
}
|
|
2960
|
+
/**
|
|
2961
|
+
* Write the scaffold for `preset` into `cwd`. NON-DESTRUCTIVE: an existing file
|
|
2962
|
+
* is left untouched and reported as `skipped` (init never clobbers your code).
|
|
2963
|
+
* Returns the per-target outcome so the command can report it and tests can
|
|
2964
|
+
* assert it. Pure of any console/spinner UI — that lives in the command wrapper.
|
|
2965
|
+
*/ async function scaffoldInit(opts) {
|
|
2966
|
+
const files = [];
|
|
2967
|
+
for (const file of buildInitFiles(opts.preset)){
|
|
2968
|
+
const target = path.join(opts.cwd, file.path);
|
|
2969
|
+
if (await fileExists(target)) {
|
|
2970
|
+
files.push({
|
|
2971
|
+
path: file.path,
|
|
2972
|
+
status: 'skipped'
|
|
2973
|
+
});
|
|
2974
|
+
continue;
|
|
2975
|
+
}
|
|
2976
|
+
await mkdir(path.dirname(target), {
|
|
2977
|
+
recursive: true
|
|
2978
|
+
});
|
|
2979
|
+
await writeFile(target, file.content(), 'utf8');
|
|
2980
|
+
files.push({
|
|
2981
|
+
path: file.path,
|
|
2982
|
+
status: 'created'
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
return {
|
|
2986
|
+
files,
|
|
2987
|
+
pkg: await patchPackageJson(opts.cwd)
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
/** Add the `cms:generate` script to package.json, if present and not already set. */ async function patchPackageJson(cwd) {
|
|
2991
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
2992
|
+
if (!await fileExists(pkgPath)) {
|
|
2993
|
+
return {
|
|
2994
|
+
status: 'skipped',
|
|
2995
|
+
reason: 'no package.json'
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
const raw = await readFile(pkgPath, 'utf8');
|
|
2999
|
+
let parsed;
|
|
3000
|
+
try {
|
|
3001
|
+
parsed = JSON.parse(raw);
|
|
3002
|
+
} catch {
|
|
3003
|
+
return {
|
|
3004
|
+
status: 'skipped',
|
|
3005
|
+
reason: 'package.json is not valid JSON'
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
3009
|
+
return {
|
|
3010
|
+
status: 'skipped',
|
|
3011
|
+
reason: 'package.json is not an object'
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
const pkg = parsed;
|
|
3015
|
+
if (pkg.scripts?.[GENERATE_SCRIPT.name]) {
|
|
3016
|
+
return {
|
|
3017
|
+
status: 'skipped',
|
|
3018
|
+
reason: `script "${GENERATE_SCRIPT.name}" exists`
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
3021
|
+
pkg.scripts = {
|
|
3022
|
+
...pkg.scripts,
|
|
3023
|
+
[GENERATE_SCRIPT.name]: GENERATE_SCRIPT.command
|
|
3024
|
+
};
|
|
3025
|
+
// Preserve the file's existing indentation (tabs / N spaces) to avoid
|
|
3026
|
+
// reformatting the user's whole package.json into a noisy git diff.
|
|
3027
|
+
const indent = raw.match(/\n([ \t]+)\S/)?.[1] ?? ' ';
|
|
3028
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, indent)}\n`, 'utf8');
|
|
3029
|
+
return {
|
|
3030
|
+
status: 'patched'
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
function registerInitCommand(cli) {
|
|
3034
|
+
cli.command('init', 'Scaffold a CMS config, a collection preset, and the Next.js route handler').option('--cwd <dir>', 'Project root to scaffold into (default: cwd)').option('--preset <name>', `Collection preset to scaffold (${Object.keys(PRESETS).join(' | ')})`).action(async (options)=>{
|
|
3035
|
+
const cwd = path.resolve(process.cwd(), options?.cwd ?? '.');
|
|
3036
|
+
const preset = await resolvePreset(options?.preset);
|
|
3037
|
+
const spinner = createSpinner(`Scaffolding the "${preset.name}" preset`);
|
|
3038
|
+
spinner.start();
|
|
3039
|
+
let result;
|
|
3040
|
+
try {
|
|
3041
|
+
result = await scaffoldInit({
|
|
3042
|
+
cwd,
|
|
3043
|
+
preset
|
|
3044
|
+
});
|
|
3045
|
+
} catch (error) {
|
|
3046
|
+
spinner.fail(' Scaffolding failed');
|
|
3047
|
+
throw error;
|
|
3048
|
+
}
|
|
3049
|
+
const created = result.files.filter((f)=>f.status === 'created').length;
|
|
3050
|
+
spinner.succeed(` Scaffolded ${created} file${created === 1 ? '' : 's'}`);
|
|
3051
|
+
console.log();
|
|
3052
|
+
for (const f of result.files){
|
|
3053
|
+
const tag = f.status === 'created' ? kleur.green('created') : kleur.yellow('exists ');
|
|
3054
|
+
printMeta(tag, kleur.dim(f.path));
|
|
3055
|
+
}
|
|
3056
|
+
const pkgTag = result.pkg.status === 'patched' ? kleur.green('script ') : kleur.yellow('skipped');
|
|
3057
|
+
printMeta(pkgTag, kleur.dim(result.pkg.status === 'patched' ? `package.json → ${GENERATE_SCRIPT.name}` : `package.json (${result.pkg.reason})`));
|
|
3058
|
+
console.log();
|
|
3059
|
+
console.log(` ${kleur.bold('Next steps')}`);
|
|
3060
|
+
printMeta('1.', 'Provide your Drizzle client at src/lib/db.ts (@/lib/db).');
|
|
3061
|
+
printMeta('2.', 'Set DATABASE_URL + the S3_* vars (see .env.example).');
|
|
3062
|
+
printMeta('3.', `Run "${GENERATE_SCRIPT.command}" to emit the Drizzle schema.`);
|
|
3063
|
+
printMeta('4.', 'Generate + run your migrations (drizzle-kit), then start the app.');
|
|
3064
|
+
console.log();
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
const cli = cac('createcms');
|
|
3069
|
+
registerInitCommand(cli);
|
|
3070
|
+
registerGenerateCommand(cli);
|
|
3071
|
+
cli.help();
|
|
3072
|
+
cli.version('0.0.1');
|
|
3073
|
+
cli.parse(process.argv, {
|
|
3074
|
+
run: false
|
|
3075
|
+
});
|
|
3076
|
+
const result = cli.runMatchedCommand();
|
|
3077
|
+
if (result && typeof result === 'object' && 'then' in result) {
|
|
3078
|
+
result.catch((error)=>{
|
|
3079
|
+
console.error(error instanceof Error ? error.message : error);
|
|
3080
|
+
process.exit(1);
|
|
3081
|
+
});
|
|
3082
|
+
}
|