@vexcms/core 0.0.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/dist/index.d.ts +2626 -0
- package/dist/index.js +3158 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3158 @@
|
|
|
1
|
+
// src/types/media.ts
|
|
2
|
+
var LOCKED_MEDIA_FIELDS = [
|
|
3
|
+
"storageId",
|
|
4
|
+
"filename",
|
|
5
|
+
"mimeType",
|
|
6
|
+
"size"
|
|
7
|
+
];
|
|
8
|
+
var OVERRIDABLE_MEDIA_FIELDS = [
|
|
9
|
+
"url",
|
|
10
|
+
"alt",
|
|
11
|
+
"width",
|
|
12
|
+
"height"
|
|
13
|
+
];
|
|
14
|
+
function getDefaultMediaFields() {
|
|
15
|
+
return {
|
|
16
|
+
storageId: {
|
|
17
|
+
type: "text",
|
|
18
|
+
required: true,
|
|
19
|
+
defaultValue: "",
|
|
20
|
+
label: "Storage ID",
|
|
21
|
+
admin: { hidden: true }
|
|
22
|
+
},
|
|
23
|
+
filename: {
|
|
24
|
+
type: "text",
|
|
25
|
+
required: true,
|
|
26
|
+
defaultValue: "",
|
|
27
|
+
label: "Filename",
|
|
28
|
+
admin: { readOnly: true }
|
|
29
|
+
},
|
|
30
|
+
mimeType: {
|
|
31
|
+
type: "text",
|
|
32
|
+
required: true,
|
|
33
|
+
defaultValue: "",
|
|
34
|
+
label: "MIME Type",
|
|
35
|
+
index: "by_mimeType",
|
|
36
|
+
admin: { readOnly: true }
|
|
37
|
+
},
|
|
38
|
+
size: {
|
|
39
|
+
type: "number",
|
|
40
|
+
required: true,
|
|
41
|
+
defaultValue: 0,
|
|
42
|
+
label: "File Size (bytes)",
|
|
43
|
+
admin: { readOnly: true }
|
|
44
|
+
},
|
|
45
|
+
url: {
|
|
46
|
+
type: "text",
|
|
47
|
+
required: true,
|
|
48
|
+
defaultValue: "",
|
|
49
|
+
label: "URL",
|
|
50
|
+
admin: { readOnly: true }
|
|
51
|
+
},
|
|
52
|
+
alt: { type: "text", label: "Alt Text" },
|
|
53
|
+
width: { type: "number", label: "Width (px)" },
|
|
54
|
+
height: { type: "number", label: "Height (px)" }
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/errors/index.ts
|
|
59
|
+
var VexError = class extends Error {
|
|
60
|
+
constructor(message) {
|
|
61
|
+
super(`[vex] ${message}`);
|
|
62
|
+
this.name = "VexError";
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var VexSlugConflictError = class extends VexError {
|
|
66
|
+
constructor(slug, existingSource, existingLocation, newSource, newLocation) {
|
|
67
|
+
super(
|
|
68
|
+
`Duplicate table slug "${slug}":
|
|
69
|
+
- ${existingSource}: ${existingLocation}
|
|
70
|
+
- ${newSource}: ${newLocation}
|
|
71
|
+
Rename one of these to resolve the conflict.`
|
|
72
|
+
);
|
|
73
|
+
this.slug = slug;
|
|
74
|
+
this.existingSource = existingSource;
|
|
75
|
+
this.existingLocation = existingLocation;
|
|
76
|
+
this.newSource = newSource;
|
|
77
|
+
this.newLocation = newLocation;
|
|
78
|
+
this.name = "VexSlugConflictError";
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var VexFieldValidationError = class extends VexError {
|
|
82
|
+
constructor(collectionSlug, fieldName, detail) {
|
|
83
|
+
super(`Field "${fieldName}" in collection "${collectionSlug}": ${detail}`);
|
|
84
|
+
this.collectionSlug = collectionSlug;
|
|
85
|
+
this.fieldName = fieldName;
|
|
86
|
+
this.detail = detail;
|
|
87
|
+
this.name = "VexFieldValidationError";
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var VexAuthConfigError = class extends VexError {
|
|
91
|
+
constructor(detail) {
|
|
92
|
+
super(`Auth configuration error: ${detail}`);
|
|
93
|
+
this.name = "VexAuthConfigError";
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var VexMediaConfigError = class extends VexError {
|
|
97
|
+
constructor(detail) {
|
|
98
|
+
super(`Media configuration error: ${detail}`);
|
|
99
|
+
this.name = "VexMediaConfigError";
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var VexAccessConfigError = class extends VexError {
|
|
103
|
+
constructor(detail) {
|
|
104
|
+
super(`Access configuration error: ${detail}`);
|
|
105
|
+
this.name = "VexAccessConfigError";
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var VexAccessError = class extends VexError {
|
|
109
|
+
constructor(resource, action, field) {
|
|
110
|
+
const target = field ? `field "${field}" on resource "${resource}"` : `resource "${resource}"`;
|
|
111
|
+
super(`Access denied: ${action} on ${target}`);
|
|
112
|
+
this.resource = resource;
|
|
113
|
+
this.action = action;
|
|
114
|
+
this.field = field;
|
|
115
|
+
this.name = "VexAccessError";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var VexBlockValidationError = class extends VexError {
|
|
119
|
+
constructor(blockSlug, detail) {
|
|
120
|
+
super(`Block "${blockSlug}": ${detail}`);
|
|
121
|
+
this.blockSlug = blockSlug;
|
|
122
|
+
this.detail = detail;
|
|
123
|
+
this.name = "VexBlockValidationError";
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// src/config/defineConfig.ts
|
|
128
|
+
var BASE_VEX_CONFIG = {
|
|
129
|
+
basePath: "/admin",
|
|
130
|
+
globals: [],
|
|
131
|
+
collections: [],
|
|
132
|
+
admin: {
|
|
133
|
+
meta: {
|
|
134
|
+
titleSuffix: "| Admin",
|
|
135
|
+
favicon: "/favicon.ico"
|
|
136
|
+
},
|
|
137
|
+
user: "users",
|
|
138
|
+
sidebar: {
|
|
139
|
+
hideGlobals: false
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
schema: {
|
|
143
|
+
outputPath: "/convex/vex.schema.ts",
|
|
144
|
+
typesOutputPath: "/convex/vex.types.ts",
|
|
145
|
+
autoMigrate: true,
|
|
146
|
+
autoRemove: false
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
function resolveMediaCollection(props) {
|
|
150
|
+
const defaults = getDefaultMediaFields();
|
|
151
|
+
if (props.mediaCollection.fields) {
|
|
152
|
+
for (const [fieldName, field] of Object.entries(props.mediaCollection.fields)) {
|
|
153
|
+
if (LOCKED_MEDIA_FIELDS.includes(fieldName)) {
|
|
154
|
+
if (process.env.NODE_ENV !== "production") {
|
|
155
|
+
console.warn(
|
|
156
|
+
`[vex] Media collection "${props.mediaCollection.slug}": field "${fieldName}" is a system field and cannot be overridden`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
defaults[fieldName] = field;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const adminConfig = {
|
|
165
|
+
...props.mediaCollection.admin,
|
|
166
|
+
useAsTitle: props.mediaCollection.admin?.useAsTitle ?? "filename"
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
slug: props.mediaCollection.slug,
|
|
170
|
+
fields: defaults,
|
|
171
|
+
tableName: props.mediaCollection.tableName,
|
|
172
|
+
labels: props.mediaCollection.labels,
|
|
173
|
+
admin: adminConfig
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function defineConfig(vexConfig) {
|
|
177
|
+
const { media: mediaInput, ...restInput } = vexConfig;
|
|
178
|
+
const config = {
|
|
179
|
+
...BASE_VEX_CONFIG,
|
|
180
|
+
...restInput,
|
|
181
|
+
admin: {
|
|
182
|
+
...BASE_VEX_CONFIG.admin,
|
|
183
|
+
...vexConfig.admin,
|
|
184
|
+
meta: {
|
|
185
|
+
...BASE_VEX_CONFIG.admin.meta,
|
|
186
|
+
...vexConfig.admin?.meta
|
|
187
|
+
},
|
|
188
|
+
sidebar: {
|
|
189
|
+
...BASE_VEX_CONFIG.admin.sidebar,
|
|
190
|
+
...vexConfig.admin?.sidebar
|
|
191
|
+
},
|
|
192
|
+
livePreview: vexConfig.admin?.livePreview
|
|
193
|
+
},
|
|
194
|
+
schema: {
|
|
195
|
+
...BASE_VEX_CONFIG.schema,
|
|
196
|
+
...vexConfig.schema
|
|
197
|
+
},
|
|
198
|
+
access: vexConfig.access
|
|
199
|
+
};
|
|
200
|
+
if (mediaInput) {
|
|
201
|
+
if (mediaInput.collections.length === 0) {
|
|
202
|
+
config.media = void 0;
|
|
203
|
+
} else if (!mediaInput.storageAdapter) {
|
|
204
|
+
throw new VexMediaConfigError(
|
|
205
|
+
"media.storageAdapter is required when media.collections is non-empty"
|
|
206
|
+
);
|
|
207
|
+
} else {
|
|
208
|
+
config.media = {
|
|
209
|
+
collections: mediaInput.collections.map(
|
|
210
|
+
(mc) => resolveMediaCollection({ mediaCollection: mc })
|
|
211
|
+
),
|
|
212
|
+
storageAdapter: mediaInput.storageAdapter
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
config.media = void 0;
|
|
217
|
+
}
|
|
218
|
+
if (process.env.NODE_ENV !== "production") {
|
|
219
|
+
for (const collection of config.collections) {
|
|
220
|
+
if (!/^[a-z][a-z0-9_]*$/.test(collection.slug)) {
|
|
221
|
+
console.warn(
|
|
222
|
+
`[vex] Collection slug "${collection.slug}" should be lowercase alphanumeric with underscores, starting with a letter`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (collection.slug.startsWith("vex_")) {
|
|
226
|
+
console.warn(
|
|
227
|
+
`[vex] Collection slug "${collection.slug}" uses reserved prefix "vex_"`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (Object.keys(collection.fields).length === 0) {
|
|
231
|
+
console.warn(`[vex] Collection "${collection.slug}" has no fields defined`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const global of config.globals) {
|
|
235
|
+
if (!/^[a-z][a-z0-9_]*$/.test(global.slug)) {
|
|
236
|
+
console.warn(
|
|
237
|
+
`[vex] Global slug "${global.slug}" should be lowercase alphanumeric with underscores, starting with a letter`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (global.slug.startsWith("vex_")) {
|
|
241
|
+
console.warn(`[vex] Global slug "${global.slug}" uses reserved prefix "vex_"`);
|
|
242
|
+
}
|
|
243
|
+
if (Object.keys(global.fields).length === 0) {
|
|
244
|
+
console.warn(`[vex] Global "${global.slug}" has no fields defined`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const slugs = config.collections.concat(config.globals).map((c) => c.slug);
|
|
248
|
+
const duplicates = slugs.filter((slug, i) => slugs.indexOf(slug) !== i);
|
|
249
|
+
if (duplicates.length > 0) {
|
|
250
|
+
console.warn(
|
|
251
|
+
`[vex] Duplicate collection slugs detected: ${duplicates.join(", ")}`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return config;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/config/defineCollection.ts
|
|
259
|
+
function defineCollection(props) {
|
|
260
|
+
const { auth: _auth, ...rest } = props;
|
|
261
|
+
return rest;
|
|
262
|
+
}
|
|
263
|
+
function defineMediaCollection(props) {
|
|
264
|
+
return props;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/access/defineAccess.ts
|
|
268
|
+
function defineAccess(props) {
|
|
269
|
+
if (props.orgCollection && !props.userOrgField) {
|
|
270
|
+
throw new VexAccessConfigError("orgCollection requires userOrgField");
|
|
271
|
+
}
|
|
272
|
+
if (props.userOrgField && !props.orgCollection) {
|
|
273
|
+
throw new VexAccessConfigError("userOrgField requires orgCollection");
|
|
274
|
+
}
|
|
275
|
+
const adminRoles = props.adminRoles ?? props.roles;
|
|
276
|
+
if (process.env.NODE_ENV !== "production") {
|
|
277
|
+
if (!props.userCollection?.slug) {
|
|
278
|
+
console.warn("[vex] defineAccess: userCollection must have a slug");
|
|
279
|
+
}
|
|
280
|
+
if (props.orgCollection && !props.orgCollection.slug) {
|
|
281
|
+
console.warn("[vex] defineAccess: orgCollection must have a slug");
|
|
282
|
+
}
|
|
283
|
+
if (props.resources) {
|
|
284
|
+
const resourceSlugs = new Set(
|
|
285
|
+
props.resources.map((r) => r.slug)
|
|
286
|
+
);
|
|
287
|
+
for (const role of Object.keys(props.permissions)) {
|
|
288
|
+
const rolePerms = props.permissions[role];
|
|
289
|
+
if (!rolePerms) continue;
|
|
290
|
+
for (const slug of Object.keys(rolePerms)) {
|
|
291
|
+
if (!resourceSlugs.has(slug)) {
|
|
292
|
+
console.warn(
|
|
293
|
+
`[vex] defineAccess: permission resource "${slug}" not found in resources`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const rolesSet = new Set(props.roles);
|
|
300
|
+
for (const role of Object.keys(props.permissions)) {
|
|
301
|
+
if (!rolesSet.has(role)) {
|
|
302
|
+
console.warn(
|
|
303
|
+
`[vex] defineAccess: permission role "${role}" not in roles array`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (props.adminRoles) {
|
|
308
|
+
const rolesSetForAdmin = new Set(props.roles);
|
|
309
|
+
for (const adminRole of props.adminRoles) {
|
|
310
|
+
if (!rolesSetForAdmin.has(adminRole)) {
|
|
311
|
+
console.warn(
|
|
312
|
+
`[vex] defineAccess: adminRole "${adminRole}" not found in roles array`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (props.userOrgField && props.userCollection?.fields) {
|
|
318
|
+
if (!(props.userOrgField in props.userCollection.fields)) {
|
|
319
|
+
console.warn(
|
|
320
|
+
`[vex] defineAccess: userOrgField "${props.userOrgField}" not found in user collection fields`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
roles: props.roles,
|
|
327
|
+
adminRoles,
|
|
328
|
+
userCollection: props.userCollection.slug,
|
|
329
|
+
orgCollection: props.orgCollection?.slug,
|
|
330
|
+
userOrgField: props.userOrgField,
|
|
331
|
+
permissions: props.permissions
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/access/hasPermission.ts
|
|
336
|
+
function resolvePermissionCheck(props) {
|
|
337
|
+
if (props.check === void 0) {
|
|
338
|
+
if (props.fields === void 0) return true;
|
|
339
|
+
return Object.fromEntries(props.fields.map((k) => [k, true]));
|
|
340
|
+
}
|
|
341
|
+
let resolved;
|
|
342
|
+
if (typeof props.check === "function") {
|
|
343
|
+
const callbackProps = props.organization !== void 0 ? { data: props.data, user: props.user, organization: props.organization } : { data: props.data, user: props.user };
|
|
344
|
+
resolved = props.check(callbackProps);
|
|
345
|
+
} else {
|
|
346
|
+
resolved = props.check;
|
|
347
|
+
}
|
|
348
|
+
if (resolved === void 0) {
|
|
349
|
+
if (props.fields === void 0) return false;
|
|
350
|
+
return Object.fromEntries(props.fields.map((k) => [k, false]));
|
|
351
|
+
}
|
|
352
|
+
if (typeof resolved === "boolean") {
|
|
353
|
+
if (props.fields === void 0) return resolved;
|
|
354
|
+
return Object.fromEntries(props.fields.map((k) => [k, resolved]));
|
|
355
|
+
}
|
|
356
|
+
if (props.fields === void 0) {
|
|
357
|
+
if (resolved.mode === "allow") return resolved.fields.length > 0;
|
|
358
|
+
if (resolved.mode === "deny") return resolved.fields.length === 0;
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
if (resolved.mode === "allow") {
|
|
362
|
+
const allowSet = new Set(resolved.fields);
|
|
363
|
+
return Object.fromEntries(
|
|
364
|
+
props.fields.map((k) => [k, allowSet.has(k)])
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const denySet = new Set(resolved.fields);
|
|
368
|
+
return Object.fromEntries(
|
|
369
|
+
props.fields.map((k) => [k, !denySet.has(k)])
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
function mergeRolePermissions(props) {
|
|
373
|
+
if (props.results.length === 0) {
|
|
374
|
+
if (props.fields === void 0) return true;
|
|
375
|
+
return Object.fromEntries(props.fields.map((k) => [k, true]));
|
|
376
|
+
}
|
|
377
|
+
if (props.fields === void 0) {
|
|
378
|
+
return props.results.some((r) => r === true);
|
|
379
|
+
}
|
|
380
|
+
return Object.fromEntries(
|
|
381
|
+
props.fields.map((k) => [
|
|
382
|
+
k,
|
|
383
|
+
props.results.some(
|
|
384
|
+
(r) => typeof r === "boolean" ? r : r[k] === true
|
|
385
|
+
)
|
|
386
|
+
])
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
function hasPermission(props) {
|
|
390
|
+
if (props.access === void 0) {
|
|
391
|
+
if (props.fields === void 0) return true;
|
|
392
|
+
return Object.fromEntries(props.fields.map((k) => [k, true]));
|
|
393
|
+
}
|
|
394
|
+
if (props.userRoles.length === 0) {
|
|
395
|
+
if (props.throwOnDenied) {
|
|
396
|
+
throw new VexAccessError(props.resource, props.action);
|
|
397
|
+
}
|
|
398
|
+
if (props.fields === void 0) return false;
|
|
399
|
+
return Object.fromEntries(props.fields.map((k) => [k, false]));
|
|
400
|
+
}
|
|
401
|
+
const knownRolesSet = new Set(props.access.roles);
|
|
402
|
+
const knownRoles = props.userRoles.filter((r) => knownRolesSet.has(r));
|
|
403
|
+
if (knownRoles.length === 0) {
|
|
404
|
+
if (props.throwOnDenied) {
|
|
405
|
+
throw new VexAccessError(props.resource, props.action);
|
|
406
|
+
}
|
|
407
|
+
if (props.fields === void 0) return false;
|
|
408
|
+
return Object.fromEntries(props.fields.map((k) => [k, false]));
|
|
409
|
+
}
|
|
410
|
+
const results = [];
|
|
411
|
+
const data = props.data ?? {};
|
|
412
|
+
for (const role of knownRoles) {
|
|
413
|
+
const rolePerms = props.access.permissions[role];
|
|
414
|
+
if (rolePerms === void 0) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const resourcePerms = rolePerms[props.resource];
|
|
418
|
+
if (resourcePerms === void 0) {
|
|
419
|
+
results.push(true);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (typeof resourcePerms === "boolean") {
|
|
423
|
+
results.push(resourcePerms);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const actionCheck = resourcePerms[props.action];
|
|
427
|
+
results.push(
|
|
428
|
+
resolvePermissionCheck({
|
|
429
|
+
check: actionCheck,
|
|
430
|
+
fields: props.fields,
|
|
431
|
+
data,
|
|
432
|
+
user: props.user,
|
|
433
|
+
organization: props.organization
|
|
434
|
+
})
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
const merged = mergeRolePermissions({
|
|
438
|
+
results,
|
|
439
|
+
fields: props.fields
|
|
440
|
+
});
|
|
441
|
+
if (props.throwOnDenied) {
|
|
442
|
+
if (typeof merged === "boolean") {
|
|
443
|
+
if (!merged) {
|
|
444
|
+
throw new VexAccessError(props.resource, props.action);
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
const deniedField = Object.entries(merged).find(([, v2]) => v2 === false);
|
|
448
|
+
if (deniedField) {
|
|
449
|
+
throw new VexAccessError(props.resource, props.action, deniedField[0]);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return merged;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/config/sanitizeConfig.ts
|
|
457
|
+
function sanitizeConfigForClient(config) {
|
|
458
|
+
const { media, ...rest } = config;
|
|
459
|
+
return {
|
|
460
|
+
...rest,
|
|
461
|
+
collections: rest.collections.map((collection) => {
|
|
462
|
+
if (!collection.admin?.livePreview) return collection;
|
|
463
|
+
if (typeof collection.admin.livePreview.url === "string") return collection;
|
|
464
|
+
return {
|
|
465
|
+
...collection,
|
|
466
|
+
admin: {
|
|
467
|
+
...collection.admin,
|
|
468
|
+
livePreview: {
|
|
469
|
+
...collection.admin.livePreview,
|
|
470
|
+
url: null
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
}),
|
|
475
|
+
media: media ? { collections: media.collections } : void 0
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function extractLivePreviewConfigs(config) {
|
|
479
|
+
const result = {};
|
|
480
|
+
for (const collection of config.collections) {
|
|
481
|
+
if (collection.admin?.livePreview && typeof collection.admin.livePreview.url === "function") {
|
|
482
|
+
result[collection.slug] = { url: collection.admin.livePreview.url };
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return result;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/config/isMediaCollection.ts
|
|
489
|
+
function isMediaCollection(props) {
|
|
490
|
+
if (!props.config.media?.collections) return false;
|
|
491
|
+
return props.config.media.collections.some(
|
|
492
|
+
(mc) => mc.slug === props.collection.slug
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/config/findCollectionBySlug.ts
|
|
497
|
+
function getAllCollections(props) {
|
|
498
|
+
const { config, excludeGlobals = false } = props;
|
|
499
|
+
const result = [];
|
|
500
|
+
for (const c of config.collections) {
|
|
501
|
+
result.push({ slug: c.slug, fields: c.fields, kind: "collection" });
|
|
502
|
+
}
|
|
503
|
+
if (config.media?.collections) {
|
|
504
|
+
for (const c of config.media.collections) {
|
|
505
|
+
result.push({ slug: c.slug, fields: c.fields, kind: "media" });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (!excludeGlobals) {
|
|
509
|
+
for (const g of config.globals) {
|
|
510
|
+
result.push({ slug: g.slug, fields: g.fields, kind: "global" });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
function findCollectionBySlug(props) {
|
|
516
|
+
return getAllCollections(props).find((c) => c.slug === props.slug) ?? null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/fields/checkbox/config.ts
|
|
520
|
+
function checkbox(options) {
|
|
521
|
+
return {
|
|
522
|
+
type: "checkbox",
|
|
523
|
+
...options?.required && options?.defaultValue === void 0 ? { defaultValue: false } : {},
|
|
524
|
+
...options
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/utils.ts
|
|
529
|
+
var MINOR_WORDS = /* @__PURE__ */ new Set([
|
|
530
|
+
"a",
|
|
531
|
+
"an",
|
|
532
|
+
"and",
|
|
533
|
+
"as",
|
|
534
|
+
// "at",
|
|
535
|
+
"but",
|
|
536
|
+
"by",
|
|
537
|
+
"for",
|
|
538
|
+
"if",
|
|
539
|
+
"in",
|
|
540
|
+
"nor",
|
|
541
|
+
"of",
|
|
542
|
+
"on",
|
|
543
|
+
"or",
|
|
544
|
+
"so",
|
|
545
|
+
"the",
|
|
546
|
+
"to",
|
|
547
|
+
"up",
|
|
548
|
+
"yet"
|
|
549
|
+
]);
|
|
550
|
+
function toTitleCase(input) {
|
|
551
|
+
const words = input.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim().split(/\s+/);
|
|
552
|
+
return words.map((word, i) => {
|
|
553
|
+
const lower = word.toLowerCase();
|
|
554
|
+
if (i > 0 && MINOR_WORDS.has(lower)) return lower;
|
|
555
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
556
|
+
}).join(" ");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/fields/checkbox/columnDef.ts
|
|
560
|
+
function checkboxColumnDef(props) {
|
|
561
|
+
return {
|
|
562
|
+
accessorKey: props.fieldKey,
|
|
563
|
+
header: props.field.label ?? toTitleCase(props.fieldKey),
|
|
564
|
+
meta: { align: props.field.admin?.cellAlignment ?? "left" }
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/valueTypes/processAdminOptions.ts
|
|
569
|
+
function processFieldValueTypeOptions(props) {
|
|
570
|
+
if (!props.field.required) {
|
|
571
|
+
return `v.optional(${props.valueType})`;
|
|
572
|
+
}
|
|
573
|
+
if (!props.skipDefaultValidation) {
|
|
574
|
+
if (props.field.defaultValue === void 0) {
|
|
575
|
+
throw new VexFieldValidationError(
|
|
576
|
+
props.collectionSlug,
|
|
577
|
+
props.fieldName,
|
|
578
|
+
"No defaultValue Provided"
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
if (!(typeof props.field.defaultValue === props.expectedType)) {
|
|
582
|
+
throw new VexFieldValidationError(
|
|
583
|
+
props.collectionSlug,
|
|
584
|
+
props.fieldName,
|
|
585
|
+
`Invalid defaultValue Provided. Expected: ${props.expectedType}, Received: ${typeof props.field.defaultValue}`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return props.valueType;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/fields/constants.ts
|
|
593
|
+
var TEXT_VALUETYPE = "v.string()";
|
|
594
|
+
var NUMBER_VALUETYPE = "v.number()";
|
|
595
|
+
var CHECKBOX_VALUETYPE = "v.boolean()";
|
|
596
|
+
var DATE_VALUETYPE = "v.number()";
|
|
597
|
+
var IMAGEURL_VALUETYPE = "v.string()";
|
|
598
|
+
var JSON_VALUETYPE = "v.any()";
|
|
599
|
+
var RICHTEXT_VALUETYPE = "v.any()";
|
|
600
|
+
|
|
601
|
+
// src/fields/checkbox/schemaValueType.ts
|
|
602
|
+
function checkboxToValueTypeString(props) {
|
|
603
|
+
return processFieldValueTypeOptions({
|
|
604
|
+
field: props.field,
|
|
605
|
+
collectionSlug: props.collectionSlug,
|
|
606
|
+
fieldName: props.fieldName,
|
|
607
|
+
expectedType: "boolean",
|
|
608
|
+
valueType: CHECKBOX_VALUETYPE
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// src/fields/number/config.ts
|
|
613
|
+
function number(options) {
|
|
614
|
+
return {
|
|
615
|
+
type: "number",
|
|
616
|
+
...options?.required && options?.defaultValue === void 0 ? { defaultValue: 0 } : {},
|
|
617
|
+
...options
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/fields/number/columnDef.ts
|
|
622
|
+
function numberColumnDef(props) {
|
|
623
|
+
return {
|
|
624
|
+
accessorKey: props.fieldKey,
|
|
625
|
+
header: props.field.label ?? toTitleCase(props.fieldKey),
|
|
626
|
+
meta: { align: props.field.admin?.cellAlignment ?? "right" }
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/fields/number/schemaValueType.ts
|
|
631
|
+
function numberToValueTypeString(props) {
|
|
632
|
+
return processFieldValueTypeOptions({
|
|
633
|
+
field: props.field,
|
|
634
|
+
collectionSlug: props.collectionSlug,
|
|
635
|
+
fieldName: props.fieldName,
|
|
636
|
+
expectedType: "number",
|
|
637
|
+
valueType: NUMBER_VALUETYPE
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/fields/select/config.ts
|
|
642
|
+
function select(options) {
|
|
643
|
+
return { type: "select", ...options };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/fields/select/columnDef.tsx
|
|
647
|
+
import { jsx } from "react/jsx-runtime";
|
|
648
|
+
var DEFAULT_BADGE_COLORS = [
|
|
649
|
+
"#3b82f6",
|
|
650
|
+
// blue
|
|
651
|
+
"#22c55e",
|
|
652
|
+
// green
|
|
653
|
+
"#a855f7",
|
|
654
|
+
// purple
|
|
655
|
+
"#f59e0b",
|
|
656
|
+
// amber
|
|
657
|
+
"#f43f5e",
|
|
658
|
+
// rose
|
|
659
|
+
"#06b6d4",
|
|
660
|
+
// cyan
|
|
661
|
+
"#6366f1",
|
|
662
|
+
// indigo
|
|
663
|
+
"#14b8a6",
|
|
664
|
+
// teal
|
|
665
|
+
"#f97316",
|
|
666
|
+
// orange
|
|
667
|
+
"#d946ef"
|
|
668
|
+
// fuchsia
|
|
669
|
+
];
|
|
670
|
+
function selectColumnDef(props) {
|
|
671
|
+
const optionMap = new Map(
|
|
672
|
+
props.field.options.map((opt, i) => [
|
|
673
|
+
opt.value,
|
|
674
|
+
{
|
|
675
|
+
label: opt.label,
|
|
676
|
+
color: opt.badgeColor ?? DEFAULT_BADGE_COLORS[i % DEFAULT_BADGE_COLORS.length]
|
|
677
|
+
}
|
|
678
|
+
])
|
|
679
|
+
);
|
|
680
|
+
return {
|
|
681
|
+
accessorKey: props.fieldKey,
|
|
682
|
+
header: (props.field.hasMany ? props.field.labels?.singular : props.field.label) ?? toTitleCase(props.fieldKey),
|
|
683
|
+
meta: {
|
|
684
|
+
align: props.field.admin?.cellAlignment ?? "left",
|
|
685
|
+
noTruncate: true
|
|
686
|
+
},
|
|
687
|
+
cell: (info) => {
|
|
688
|
+
const raw = info.getValue();
|
|
689
|
+
const values = Array.isArray(raw) ? raw : raw != null ? [String(raw)] : [];
|
|
690
|
+
if (values.length === 0) return null;
|
|
691
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1 max-w-[240px] max-h-[60px] overflow-auto", children: values.map((v2) => {
|
|
692
|
+
const opt = optionMap.get(v2);
|
|
693
|
+
return /* @__PURE__ */ jsx(
|
|
694
|
+
"span",
|
|
695
|
+
{
|
|
696
|
+
className: "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium text-white shrink-0",
|
|
697
|
+
style: { backgroundColor: opt?.color },
|
|
698
|
+
children: opt?.label ?? v2
|
|
699
|
+
},
|
|
700
|
+
v2
|
|
701
|
+
);
|
|
702
|
+
}) });
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/fields/select/schemaValueType.ts
|
|
708
|
+
function selectToValueTypeString(props) {
|
|
709
|
+
const literals = props.field.options.map((o) => `v.literal("${o.value}")`).join(",");
|
|
710
|
+
if (props.field.hasMany) {
|
|
711
|
+
return processFieldValueTypeOptions({
|
|
712
|
+
field: props.field,
|
|
713
|
+
collectionSlug: props.collectionSlug,
|
|
714
|
+
fieldName: props.fieldName,
|
|
715
|
+
expectedType: "object",
|
|
716
|
+
valueType: props.field.options.length === 1 ? `v.array(${literals})` : `v.array(v.union(${literals}))`,
|
|
717
|
+
skipDefaultValidation: true
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
return processFieldValueTypeOptions({
|
|
721
|
+
field: props.field,
|
|
722
|
+
collectionSlug: props.collectionSlug,
|
|
723
|
+
fieldName: props.fieldName,
|
|
724
|
+
expectedType: "string",
|
|
725
|
+
valueType: `v.union(${literals})`
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/fields/text/config.ts
|
|
730
|
+
function text(options) {
|
|
731
|
+
return {
|
|
732
|
+
type: "text",
|
|
733
|
+
...options?.required && options?.defaultValue === void 0 ? { defaultValue: "" } : {},
|
|
734
|
+
...options
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/fields/text/columnDef.tsx
|
|
739
|
+
function textColumnDef(props) {
|
|
740
|
+
return {
|
|
741
|
+
accessorKey: props.fieldKey,
|
|
742
|
+
header: props.field.label ?? toTitleCase(props.fieldKey),
|
|
743
|
+
meta: { align: props.field.admin?.cellAlignment ?? "left" }
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/fields/text/schemaValueType.ts
|
|
748
|
+
function textToValueTypeString(props) {
|
|
749
|
+
return processFieldValueTypeOptions({
|
|
750
|
+
field: props.field,
|
|
751
|
+
collectionSlug: props.collectionSlug,
|
|
752
|
+
fieldName: props.fieldName,
|
|
753
|
+
expectedType: "string",
|
|
754
|
+
valueType: TEXT_VALUETYPE
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/fields/date/config.ts
|
|
759
|
+
function date(options) {
|
|
760
|
+
return {
|
|
761
|
+
type: "date",
|
|
762
|
+
...options?.required && options?.defaultValue === void 0 ? { defaultValue: 0 } : {},
|
|
763
|
+
...options
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/fields/date/columnDef.ts
|
|
768
|
+
function dateColumnDef(props) {
|
|
769
|
+
return {
|
|
770
|
+
accessorKey: props.fieldKey,
|
|
771
|
+
header: props.field.label ?? toTitleCase(props.fieldKey),
|
|
772
|
+
meta: {
|
|
773
|
+
align: props.field.admin?.cellAlignment ?? "left"
|
|
774
|
+
},
|
|
775
|
+
cell: (info) => {
|
|
776
|
+
const value = info.getValue();
|
|
777
|
+
if (value == null) return "";
|
|
778
|
+
return new Date(value).toLocaleDateString();
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/fields/date/schemaValueType.ts
|
|
784
|
+
function dateToValueTypeString(props) {
|
|
785
|
+
return processFieldValueTypeOptions({
|
|
786
|
+
field: props.field,
|
|
787
|
+
collectionSlug: props.collectionSlug,
|
|
788
|
+
fieldName: props.fieldName,
|
|
789
|
+
expectedType: "number",
|
|
790
|
+
valueType: DATE_VALUETYPE
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/fields/imageUrl/config.ts
|
|
795
|
+
function imageUrl(options) {
|
|
796
|
+
return {
|
|
797
|
+
type: "imageUrl",
|
|
798
|
+
...options?.required && options?.defaultValue === void 0 ? { defaultValue: "" } : {},
|
|
799
|
+
...options
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/fields/imageUrl/columnDef.tsx
|
|
804
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
805
|
+
function imageUrlColumnDef(props) {
|
|
806
|
+
return {
|
|
807
|
+
accessorKey: props.fieldKey,
|
|
808
|
+
header: props.field.label ?? toTitleCase(props.fieldKey),
|
|
809
|
+
meta: { align: props.field.admin?.cellAlignment ?? "center" },
|
|
810
|
+
cell: (info) => {
|
|
811
|
+
const value = info.getValue();
|
|
812
|
+
if (!value || typeof value !== "string") return "";
|
|
813
|
+
const size = props.field.width ?? 28;
|
|
814
|
+
const height = props.field.height ?? size;
|
|
815
|
+
return /* @__PURE__ */ jsx2(
|
|
816
|
+
"img",
|
|
817
|
+
{
|
|
818
|
+
src: value,
|
|
819
|
+
alt: "",
|
|
820
|
+
width: size,
|
|
821
|
+
height,
|
|
822
|
+
className: "rounded-full object-cover bg-muted",
|
|
823
|
+
style: { width: size, height },
|
|
824
|
+
loading: "lazy",
|
|
825
|
+
referrerPolicy: "no-referrer",
|
|
826
|
+
onError: (e) => {
|
|
827
|
+
e.currentTarget.style.display = "none";
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/fields/imageUrl/schemaValueType.ts
|
|
836
|
+
function imageUrlToValueTypeString(props) {
|
|
837
|
+
return processFieldValueTypeOptions({
|
|
838
|
+
field: props.field,
|
|
839
|
+
collectionSlug: props.collectionSlug,
|
|
840
|
+
fieldName: props.fieldName,
|
|
841
|
+
expectedType: "string",
|
|
842
|
+
valueType: IMAGEURL_VALUETYPE
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/fields/relationship/config.ts
|
|
847
|
+
function relationship(options) {
|
|
848
|
+
return { type: "relationship", ...options };
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// src/fields/relationship/columnDef.ts
|
|
852
|
+
function relationshipColumnDef(props) {
|
|
853
|
+
return {
|
|
854
|
+
accessorKey: props.fieldKey,
|
|
855
|
+
header: (props.field.hasMany ? props.field.labels?.singular : props.field.label) ?? toTitleCase(props.fieldKey),
|
|
856
|
+
meta: { type: "relationship", to: props.field.to, align: props.field.admin?.cellAlignment ?? "left" },
|
|
857
|
+
cell: (info) => {
|
|
858
|
+
const value = info.getValue();
|
|
859
|
+
if (value == null) return "";
|
|
860
|
+
if (Array.isArray(value)) return `${value.length} item${value.length === 1 ? "" : "s"}`;
|
|
861
|
+
return String(value);
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/fields/relationship/schemaValueType.ts
|
|
867
|
+
function relationshipToValueTypeString(props) {
|
|
868
|
+
const idType = `v.id("${props.field.to}")`;
|
|
869
|
+
const baseValueType = props.field.hasMany ? `v.array(${idType})` : idType;
|
|
870
|
+
return processFieldValueTypeOptions({
|
|
871
|
+
field: props.field,
|
|
872
|
+
collectionSlug: props.collectionSlug,
|
|
873
|
+
fieldName: props.fieldName,
|
|
874
|
+
expectedType: "string",
|
|
875
|
+
valueType: baseValueType,
|
|
876
|
+
skipDefaultValidation: true
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/fields/json/config.ts
|
|
881
|
+
function json(options) {
|
|
882
|
+
return {
|
|
883
|
+
type: "json",
|
|
884
|
+
...options
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/fields/json/columnDef.ts
|
|
889
|
+
function jsonColumnDef(props) {
|
|
890
|
+
return {
|
|
891
|
+
accessorKey: props.fieldKey,
|
|
892
|
+
header: props.field.label ?? toTitleCase(props.fieldKey),
|
|
893
|
+
meta: { align: props.field.admin?.cellAlignment ?? "left" },
|
|
894
|
+
cell: (info) => {
|
|
895
|
+
const value = info.getValue();
|
|
896
|
+
if (value == null) return "";
|
|
897
|
+
const str = JSON.stringify(value);
|
|
898
|
+
return str.length > 50 ? str.slice(0, 50) + "..." : str;
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/fields/json/schemaValueType.ts
|
|
904
|
+
function jsonToValueTypeString(props) {
|
|
905
|
+
return processFieldValueTypeOptions({
|
|
906
|
+
field: props.field,
|
|
907
|
+
collectionSlug: props.collectionSlug,
|
|
908
|
+
fieldName: props.fieldName,
|
|
909
|
+
expectedType: "object",
|
|
910
|
+
valueType: JSON_VALUETYPE,
|
|
911
|
+
skipDefaultValidation: true
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/fields/richtext/config.ts
|
|
916
|
+
function richtext(options) {
|
|
917
|
+
return {
|
|
918
|
+
type: "richtext",
|
|
919
|
+
...options
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// src/fields/richtext/schemaValueType.ts
|
|
924
|
+
function richtextToValueTypeString(props) {
|
|
925
|
+
return processFieldValueTypeOptions({
|
|
926
|
+
field: props.field,
|
|
927
|
+
collectionSlug: props.collectionSlug,
|
|
928
|
+
fieldName: props.fieldName,
|
|
929
|
+
expectedType: "object",
|
|
930
|
+
valueType: RICHTEXT_VALUETYPE,
|
|
931
|
+
skipDefaultValidation: true
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// src/fields/richtext/columnDef.ts
|
|
936
|
+
function richtextColumnDef(props) {
|
|
937
|
+
return {
|
|
938
|
+
accessorKey: props.fieldKey,
|
|
939
|
+
header: props.field.label ?? toTitleCase(props.fieldKey),
|
|
940
|
+
meta: { align: props.field.admin?.cellAlignment ?? "left" },
|
|
941
|
+
cell: (info) => {
|
|
942
|
+
const value = info.getValue();
|
|
943
|
+
if (value == null) return "";
|
|
944
|
+
if (Array.isArray(value) && value.length === 0) return "";
|
|
945
|
+
return "Rich text";
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/fields/media/config.ts
|
|
951
|
+
function upload(options) {
|
|
952
|
+
return { type: "upload", ...options };
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// src/fields/media/schemaValueType.ts
|
|
956
|
+
function uploadToValueTypeString(props) {
|
|
957
|
+
const idType = `v.id("${props.field.to}")`;
|
|
958
|
+
const baseValueType = props.field.hasMany ? `v.array(${idType})` : idType;
|
|
959
|
+
return processFieldValueTypeOptions({
|
|
960
|
+
field: props.field,
|
|
961
|
+
collectionSlug: props.collectionSlug,
|
|
962
|
+
fieldName: props.fieldName,
|
|
963
|
+
expectedType: "string",
|
|
964
|
+
valueType: baseValueType,
|
|
965
|
+
skipDefaultValidation: true
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/fields/array/config.ts
|
|
970
|
+
function array(options) {
|
|
971
|
+
return { type: "array", ...options };
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/fields/array/columnDef.ts
|
|
975
|
+
function arrayColumnDef(props) {
|
|
976
|
+
return {
|
|
977
|
+
accessorKey: props.fieldKey,
|
|
978
|
+
header: props.field.label ?? toTitleCase(props.fieldKey),
|
|
979
|
+
meta: { align: props.field.admin?.cellAlignment ?? "left" },
|
|
980
|
+
cell: (info) => {
|
|
981
|
+
const value = info.getValue();
|
|
982
|
+
if (!Array.isArray(value) || value.length === 0) return "no items";
|
|
983
|
+
if (value.length === 1) return "1 item";
|
|
984
|
+
return `${value.length} items`;
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/fields/array/schemaValueType.ts
|
|
990
|
+
function arrayToValueTypeString(props) {
|
|
991
|
+
const innerValueType = props.resolveInnerField({
|
|
992
|
+
field: props.field.field,
|
|
993
|
+
collectionSlug: props.collectionSlug,
|
|
994
|
+
fieldName: `${props.fieldName}[]`
|
|
995
|
+
});
|
|
996
|
+
const unwrapped = innerValueType.replace(/^v\.optional\((.+)\)$/, "$1");
|
|
997
|
+
const arrayType = `v.array(${unwrapped})`;
|
|
998
|
+
return processFieldValueTypeOptions({
|
|
999
|
+
field: props.field,
|
|
1000
|
+
collectionSlug: props.collectionSlug,
|
|
1001
|
+
fieldName: props.fieldName,
|
|
1002
|
+
expectedType: "object",
|
|
1003
|
+
valueType: arrayType,
|
|
1004
|
+
skipDefaultValidation: true
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/fields/blocks/config.ts
|
|
1009
|
+
function blocks(props) {
|
|
1010
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1011
|
+
for (const block of props.blocks) {
|
|
1012
|
+
if (seen.has(block.slug)) {
|
|
1013
|
+
throw new VexBlockValidationError(
|
|
1014
|
+
block.slug,
|
|
1015
|
+
`Duplicate block slug "${block.slug}" in blocks field. Each block in a blocks() field must have a unique slug.`
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
seen.add(block.slug);
|
|
1019
|
+
}
|
|
1020
|
+
return {
|
|
1021
|
+
type: "blocks",
|
|
1022
|
+
blocks: props.blocks,
|
|
1023
|
+
labels: props.labels,
|
|
1024
|
+
min: props.min,
|
|
1025
|
+
max: props.max,
|
|
1026
|
+
label: props.label,
|
|
1027
|
+
description: props.description,
|
|
1028
|
+
required: props.required,
|
|
1029
|
+
admin: props.admin
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// src/fields/blocks/schemaValueType.ts
|
|
1034
|
+
function blocksToValueTypeString(props) {
|
|
1035
|
+
const visited = props.visitedBlockSlugs ?? /* @__PURE__ */ new Set();
|
|
1036
|
+
if (props.field.blocks.length === 0) {
|
|
1037
|
+
return processFieldValueTypeOptions({
|
|
1038
|
+
field: props.field,
|
|
1039
|
+
collectionSlug: props.collectionSlug,
|
|
1040
|
+
fieldName: props.fieldName,
|
|
1041
|
+
expectedType: "object",
|
|
1042
|
+
valueType: "v.array(v.any())",
|
|
1043
|
+
skipDefaultValidation: true
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
const objectTypes = [];
|
|
1047
|
+
for (const block of props.field.blocks) {
|
|
1048
|
+
if (visited.has(block.slug)) {
|
|
1049
|
+
throw new VexBlockValidationError(
|
|
1050
|
+
block.slug,
|
|
1051
|
+
`Circular block reference detected: block "${block.slug}" references itself (directly or through a cycle).`
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
const blockVisited = new Set(visited);
|
|
1055
|
+
blockVisited.add(block.slug);
|
|
1056
|
+
const fieldEntries = [
|
|
1057
|
+
`blockType: v.literal("${block.slug}")`,
|
|
1058
|
+
`blockName: v.optional(v.string())`,
|
|
1059
|
+
`_key: v.string()`
|
|
1060
|
+
];
|
|
1061
|
+
for (const [fieldName, field] of Object.entries(block.fields)) {
|
|
1062
|
+
const valueType = props.resolveInnerField({
|
|
1063
|
+
field,
|
|
1064
|
+
collectionSlug: props.collectionSlug,
|
|
1065
|
+
fieldName: `${props.fieldName}.${block.slug}.${fieldName}`,
|
|
1066
|
+
visitedBlockSlugs: blockVisited
|
|
1067
|
+
});
|
|
1068
|
+
fieldEntries.push(`${fieldName}: ${valueType}`);
|
|
1069
|
+
}
|
|
1070
|
+
objectTypes.push(`v.object({${fieldEntries.join(", ")}})`);
|
|
1071
|
+
}
|
|
1072
|
+
const innerType = objectTypes.length === 1 ? objectTypes[0] : `v.union(${objectTypes.join(", ")})`;
|
|
1073
|
+
const arrayType = `v.array(${innerType})`;
|
|
1074
|
+
return processFieldValueTypeOptions({
|
|
1075
|
+
field: props.field,
|
|
1076
|
+
collectionSlug: props.collectionSlug,
|
|
1077
|
+
fieldName: props.fieldName,
|
|
1078
|
+
expectedType: "object",
|
|
1079
|
+
valueType: arrayType,
|
|
1080
|
+
skipDefaultValidation: true
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/fields/blocks/columnDef.ts
|
|
1085
|
+
function blocksColumnDef(props) {
|
|
1086
|
+
const singular = props.field.labels?.singular ?? "block";
|
|
1087
|
+
const plural = props.field.labels?.plural ?? "blocks";
|
|
1088
|
+
return {
|
|
1089
|
+
accessorKey: props.fieldKey,
|
|
1090
|
+
header: props.field.label ?? toTitleCase(props.fieldKey),
|
|
1091
|
+
meta: { align: props.field.admin?.cellAlignment ?? "left" },
|
|
1092
|
+
cell: (info) => {
|
|
1093
|
+
const value = info.getValue();
|
|
1094
|
+
if (!Array.isArray(value) || value.length === 0) return `no ${plural}`;
|
|
1095
|
+
if (value.length === 1) return `1 ${singular}`;
|
|
1096
|
+
return `${value.length} ${plural}`;
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/valueTypes/extract.ts
|
|
1102
|
+
function fieldToValueType(props) {
|
|
1103
|
+
const { field, collectionSlug, fieldName } = props;
|
|
1104
|
+
switch (field.type) {
|
|
1105
|
+
case "text":
|
|
1106
|
+
return textToValueTypeString({ field, collectionSlug, fieldName });
|
|
1107
|
+
case "number":
|
|
1108
|
+
return numberToValueTypeString({ field, collectionSlug, fieldName });
|
|
1109
|
+
case "checkbox":
|
|
1110
|
+
return checkboxToValueTypeString({ field, collectionSlug, fieldName });
|
|
1111
|
+
case "select":
|
|
1112
|
+
return selectToValueTypeString({ field, collectionSlug, fieldName });
|
|
1113
|
+
case "date":
|
|
1114
|
+
return dateToValueTypeString({ field, collectionSlug, fieldName });
|
|
1115
|
+
case "imageUrl":
|
|
1116
|
+
return imageUrlToValueTypeString({ field, collectionSlug, fieldName });
|
|
1117
|
+
case "relationship":
|
|
1118
|
+
return relationshipToValueTypeString({ field, collectionSlug, fieldName });
|
|
1119
|
+
case "upload":
|
|
1120
|
+
return uploadToValueTypeString({ field, collectionSlug, fieldName });
|
|
1121
|
+
case "json":
|
|
1122
|
+
return jsonToValueTypeString({ field, collectionSlug, fieldName });
|
|
1123
|
+
case "richtext":
|
|
1124
|
+
return richtextToValueTypeString({ field, collectionSlug, fieldName });
|
|
1125
|
+
case "array":
|
|
1126
|
+
return arrayToValueTypeString({
|
|
1127
|
+
field,
|
|
1128
|
+
collectionSlug,
|
|
1129
|
+
fieldName,
|
|
1130
|
+
resolveInnerField: fieldToValueType
|
|
1131
|
+
});
|
|
1132
|
+
case "blocks":
|
|
1133
|
+
return blocksToValueTypeString({
|
|
1134
|
+
field,
|
|
1135
|
+
collectionSlug,
|
|
1136
|
+
fieldName,
|
|
1137
|
+
resolveInnerField: (innerProps) => fieldToValueType({
|
|
1138
|
+
field: innerProps.field,
|
|
1139
|
+
collectionSlug: innerProps.collectionSlug,
|
|
1140
|
+
fieldName: innerProps.fieldName,
|
|
1141
|
+
visitedBlockSlugs: innerProps.visitedBlockSlugs
|
|
1142
|
+
}),
|
|
1143
|
+
visitedBlockSlugs: props.visitedBlockSlugs
|
|
1144
|
+
});
|
|
1145
|
+
case "ui":
|
|
1146
|
+
throw new VexFieldValidationError(
|
|
1147
|
+
collectionSlug,
|
|
1148
|
+
fieldName,
|
|
1149
|
+
`UI field "${fieldName}" on collection "${collectionSlug}" has no database representation and should not be included in schema generation.`
|
|
1150
|
+
);
|
|
1151
|
+
default:
|
|
1152
|
+
throw new VexFieldValidationError(
|
|
1153
|
+
collectionSlug,
|
|
1154
|
+
fieldName,
|
|
1155
|
+
`Unknown Field Type: ${field.type}`
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// src/valueTypes/indexes.ts
|
|
1161
|
+
function collectIndexes(props) {
|
|
1162
|
+
const { collection } = props;
|
|
1163
|
+
const fieldIndexes = /* @__PURE__ */ new Map();
|
|
1164
|
+
for (const [fieldKey, field] of Object.entries(collection.fields)) {
|
|
1165
|
+
const indexName = field.index;
|
|
1166
|
+
if (indexName) {
|
|
1167
|
+
if (fieldIndexes.has(indexName)) {
|
|
1168
|
+
throw new VexFieldValidationError(
|
|
1169
|
+
collection.slug,
|
|
1170
|
+
fieldKey,
|
|
1171
|
+
`Duplicate Indexes detected: ${indexName}`
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
fieldIndexes.set(indexName, { name: indexName, fields: [fieldKey] });
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
collection.indexes?.forEach((index) => {
|
|
1178
|
+
fieldIndexes.set(index.name, { name: index.name, fields: index.fields });
|
|
1179
|
+
});
|
|
1180
|
+
const useAsTitle = collection.admin?.useAsTitle;
|
|
1181
|
+
if (useAsTitle && useAsTitle !== "_id") {
|
|
1182
|
+
const autoName = `by_${useAsTitle}`;
|
|
1183
|
+
if (!fieldIndexes.has(autoName)) {
|
|
1184
|
+
fieldIndexes.set(autoName, { name: autoName, fields: [useAsTitle] });
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return Array.from(fieldIndexes.values());
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// src/valueTypes/searchIndexes.ts
|
|
1191
|
+
function collectSearchIndexes(props) {
|
|
1192
|
+
const { collection } = props;
|
|
1193
|
+
const searchIndexes = /* @__PURE__ */ new Map();
|
|
1194
|
+
for (const [fieldKey, field] of Object.entries(collection.fields)) {
|
|
1195
|
+
const searchIndex = field.searchIndex;
|
|
1196
|
+
if (searchIndex && searchIndex.name) {
|
|
1197
|
+
if (searchIndexes.has(searchIndex.name)) {
|
|
1198
|
+
throw new VexFieldValidationError(
|
|
1199
|
+
collection.slug,
|
|
1200
|
+
fieldKey,
|
|
1201
|
+
`Duplicate search index name: ${searchIndex.name}`
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
searchIndexes.set(searchIndex.name, {
|
|
1205
|
+
name: searchIndex.name,
|
|
1206
|
+
searchField: fieldKey,
|
|
1207
|
+
filterFields: searchIndex.filterFields
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
collection.searchIndexes?.forEach((entry) => {
|
|
1212
|
+
searchIndexes.set(entry.name, {
|
|
1213
|
+
name: entry.name,
|
|
1214
|
+
searchField: entry.searchField,
|
|
1215
|
+
filterFields: entry.filterFields ?? []
|
|
1216
|
+
});
|
|
1217
|
+
});
|
|
1218
|
+
const useAsTitle = collection.admin?.useAsTitle;
|
|
1219
|
+
if (useAsTitle && useAsTitle !== "_id") {
|
|
1220
|
+
const autoName = `search_${useAsTitle}`;
|
|
1221
|
+
const alreadyCovered = Array.from(searchIndexes.values()).some(
|
|
1222
|
+
(si) => si.searchField === useAsTitle
|
|
1223
|
+
);
|
|
1224
|
+
if (!alreadyCovered && !searchIndexes.has(autoName)) {
|
|
1225
|
+
searchIndexes.set(autoName, {
|
|
1226
|
+
name: autoName,
|
|
1227
|
+
searchField: useAsTitle,
|
|
1228
|
+
filterFields: []
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return Array.from(searchIndexes.values());
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// src/valueTypes/merge.ts
|
|
1236
|
+
function mergeAuthCollectionWithUserCollection(props) {
|
|
1237
|
+
const { authCollection, userCollection } = props;
|
|
1238
|
+
const fields = {};
|
|
1239
|
+
const overlapping = [];
|
|
1240
|
+
const authOnly = [];
|
|
1241
|
+
const userOnly = [];
|
|
1242
|
+
const authFields = authCollection.fields;
|
|
1243
|
+
const userFields = userCollection.fields;
|
|
1244
|
+
const authFieldKeys = Object.keys(authFields);
|
|
1245
|
+
const userFieldKeys = Object.keys(userFields);
|
|
1246
|
+
for (const fieldKey of authFieldKeys) {
|
|
1247
|
+
if (userFieldKeys.includes(fieldKey)) {
|
|
1248
|
+
overlapping.push(fieldKey);
|
|
1249
|
+
const authField = authFields[fieldKey];
|
|
1250
|
+
const userField = userFields[fieldKey];
|
|
1251
|
+
fields[fieldKey] = {
|
|
1252
|
+
...userField,
|
|
1253
|
+
required: authField.required,
|
|
1254
|
+
...authField.defaultValue !== void 0 && { defaultValue: authField.defaultValue }
|
|
1255
|
+
};
|
|
1256
|
+
} else {
|
|
1257
|
+
authOnly.push(fieldKey);
|
|
1258
|
+
fields[fieldKey] = authFields[fieldKey];
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
for (const fieldKey of userFieldKeys) {
|
|
1262
|
+
if (authFieldKeys.includes(fieldKey)) continue;
|
|
1263
|
+
userOnly.push(fieldKey);
|
|
1264
|
+
fields[fieldKey] = userFields[fieldKey];
|
|
1265
|
+
}
|
|
1266
|
+
const indexes = [];
|
|
1267
|
+
const indexNames = /* @__PURE__ */ new Set();
|
|
1268
|
+
for (const idx of authCollection.indexes ?? []) {
|
|
1269
|
+
indexes.push({ name: idx.name, fields: idx.fields });
|
|
1270
|
+
indexNames.add(idx.name);
|
|
1271
|
+
}
|
|
1272
|
+
for (const idx of userCollection.indexes ?? []) {
|
|
1273
|
+
if (!indexNames.has(idx.name)) {
|
|
1274
|
+
indexes.push({ name: idx.name, fields: idx.fields });
|
|
1275
|
+
indexNames.add(idx.name);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
const searchIndexes = (userCollection.searchIndexes ?? []).map((si) => ({
|
|
1279
|
+
name: si.name,
|
|
1280
|
+
searchField: si.searchField,
|
|
1281
|
+
filterFields: si.filterFields ?? []
|
|
1282
|
+
}));
|
|
1283
|
+
return { fields, indexes, searchIndexes, overlapping, authOnly, userOnly };
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// src/valueTypes/slugs.ts
|
|
1287
|
+
var SLUG_SOURCES = {
|
|
1288
|
+
userCollection: "user-collection",
|
|
1289
|
+
userGlobal: "user-global",
|
|
1290
|
+
authTable: "auth-table",
|
|
1291
|
+
mediaCollection: "media-collection",
|
|
1292
|
+
system: "system"
|
|
1293
|
+
};
|
|
1294
|
+
var SlugRegistry = class {
|
|
1295
|
+
registrations = /* @__PURE__ */ new Map();
|
|
1296
|
+
/**
|
|
1297
|
+
* Register a slug with its source.
|
|
1298
|
+
* Throws VexSlugConflictError immediately if the slug is already registered,
|
|
1299
|
+
* UNLESS an auth table slug overlaps with a user collection slug — this is
|
|
1300
|
+
* expected behavior indicating the user wants to customize that auth table's
|
|
1301
|
+
* admin UI. In that case, the user collection's registration takes precedence
|
|
1302
|
+
* (it was registered first as "user-collection") and the auth table is
|
|
1303
|
+
* silently skipped in the registry. The merge happens during schema generation.
|
|
1304
|
+
*
|
|
1305
|
+
* @param props.slug - The table slug to register
|
|
1306
|
+
* @param props.source - Where this slug comes from (e.g., "user-collection", "auth-table")
|
|
1307
|
+
* @param props.location - Human-readable location for error messages (e.g., `collection "posts"`)
|
|
1308
|
+
*
|
|
1309
|
+
* Edge cases:
|
|
1310
|
+
* - Auth table slug matches user collection slug: NOT a conflict — skip
|
|
1311
|
+
* registration (user collection already registered, merge happens later)
|
|
1312
|
+
* - System table prefixed with "vex_" should not conflict with user tables
|
|
1313
|
+
* because defineCollection already warns about "vex_" prefix
|
|
1314
|
+
*/
|
|
1315
|
+
register(props) {
|
|
1316
|
+
const existing = this.registrations.get(props.slug);
|
|
1317
|
+
if (existing) {
|
|
1318
|
+
if (existing.source === "user-collection" && props.source === "auth-table" || existing.source === "auth-table" && props.source === "user-collection") {
|
|
1319
|
+
if (props.source === "user-collection") {
|
|
1320
|
+
this.registrations.set(props.slug, {
|
|
1321
|
+
slug: props.slug,
|
|
1322
|
+
source: props.source,
|
|
1323
|
+
location: props.location
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
throw new VexSlugConflictError(
|
|
1329
|
+
props.slug,
|
|
1330
|
+
existing.source,
|
|
1331
|
+
existing.location,
|
|
1332
|
+
props.source,
|
|
1333
|
+
props.location
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
this.registrations.set(props.slug, {
|
|
1337
|
+
slug: props.slug,
|
|
1338
|
+
source: props.source,
|
|
1339
|
+
location: props.location
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Get all registered slugs.
|
|
1344
|
+
*/
|
|
1345
|
+
getAll() {
|
|
1346
|
+
return [...this.registrations.values()];
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
function buildSlugRegistry(props) {
|
|
1350
|
+
const registry = new SlugRegistry();
|
|
1351
|
+
for (const collection of props.config.collections) {
|
|
1352
|
+
registry.register({
|
|
1353
|
+
slug: collection.slug,
|
|
1354
|
+
source: SLUG_SOURCES.userCollection,
|
|
1355
|
+
location: `Collection ${collection.slug}`
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
if (props.config.media) {
|
|
1359
|
+
for (const collection of props.config.media.collections) {
|
|
1360
|
+
registry.register({
|
|
1361
|
+
slug: collection.slug,
|
|
1362
|
+
source: SLUG_SOURCES.mediaCollection,
|
|
1363
|
+
location: `Media Collection ${collection.slug}`
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
for (const global of props.config.globals) {
|
|
1368
|
+
registry.register({
|
|
1369
|
+
slug: global.slug,
|
|
1370
|
+
source: SLUG_SOURCES.userGlobal,
|
|
1371
|
+
location: `Global ${global.slug}`
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
for (const collection of props.config.auth.collections) {
|
|
1375
|
+
registry.register({
|
|
1376
|
+
slug: collection.slug,
|
|
1377
|
+
source: SLUG_SOURCES.authTable,
|
|
1378
|
+
location: `Auth Table ${collection.slug}`
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
return registry;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// src/valueTypes/generate.ts
|
|
1385
|
+
function generateVexSchema(props) {
|
|
1386
|
+
const config = props.config;
|
|
1387
|
+
buildSlugRegistry({ config });
|
|
1388
|
+
const authCollectionMap = new Map(
|
|
1389
|
+
config.auth.collections.map((c) => [c.slug, c])
|
|
1390
|
+
);
|
|
1391
|
+
const mergedAuthSlugs = /* @__PURE__ */ new Set();
|
|
1392
|
+
const lines = [
|
|
1393
|
+
"// \u26A0\uFE0F AUTO-GENERATED BY VEX CMS \u2014 DO NOT EDIT \u26A0\uFE0F",
|
|
1394
|
+
"",
|
|
1395
|
+
'import { defineTable } from "convex/server";',
|
|
1396
|
+
'import { v } from "convex/values";'
|
|
1397
|
+
];
|
|
1398
|
+
if (config.collections.length > 0) {
|
|
1399
|
+
lines.push("", "/**", " * USER COLLECTIONS", " **/");
|
|
1400
|
+
}
|
|
1401
|
+
for (const collection of config.collections) {
|
|
1402
|
+
const authCollection = authCollectionMap.get(collection.slug);
|
|
1403
|
+
const fields = [];
|
|
1404
|
+
const indexes = collectIndexes({ collection });
|
|
1405
|
+
const searchIndexes = collectSearchIndexes({
|
|
1406
|
+
collection
|
|
1407
|
+
});
|
|
1408
|
+
if (authCollection) {
|
|
1409
|
+
mergedAuthSlugs.add(authCollection.slug);
|
|
1410
|
+
const merged = mergeAuthCollectionWithUserCollection({
|
|
1411
|
+
authCollection,
|
|
1412
|
+
userCollection: collection
|
|
1413
|
+
});
|
|
1414
|
+
for (const [name, field] of Object.entries(merged.fields)) {
|
|
1415
|
+
if (field.type === "ui") continue;
|
|
1416
|
+
fields.push({
|
|
1417
|
+
name,
|
|
1418
|
+
valueType: fieldToValueType({
|
|
1419
|
+
field,
|
|
1420
|
+
collectionSlug: collection.slug,
|
|
1421
|
+
fieldName: name
|
|
1422
|
+
})
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
for (const index of merged.indexes) {
|
|
1426
|
+
if (indexes.find((ui2) => ui2.name === index.name)) continue;
|
|
1427
|
+
indexes.push(index);
|
|
1428
|
+
}
|
|
1429
|
+
for (const si of merged.searchIndexes) {
|
|
1430
|
+
if (searchIndexes.find((existing) => existing.name === si.name))
|
|
1431
|
+
continue;
|
|
1432
|
+
searchIndexes.push(si);
|
|
1433
|
+
}
|
|
1434
|
+
} else {
|
|
1435
|
+
for (const [fieldName, field] of Object.entries(
|
|
1436
|
+
collection.fields
|
|
1437
|
+
)) {
|
|
1438
|
+
if (field.type === "ui") continue;
|
|
1439
|
+
fields.push({
|
|
1440
|
+
name: fieldName,
|
|
1441
|
+
valueType: fieldToValueType({
|
|
1442
|
+
fieldName,
|
|
1443
|
+
field,
|
|
1444
|
+
collectionSlug: collection.slug
|
|
1445
|
+
})
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
lines.push(
|
|
1450
|
+
"",
|
|
1451
|
+
`export const ${collection.tableName ?? collection.slug} = defineTable({`
|
|
1452
|
+
);
|
|
1453
|
+
for (const f of fields) {
|
|
1454
|
+
lines.push(` ${f.name}: ${f.valueType},`);
|
|
1455
|
+
}
|
|
1456
|
+
lines.push(` vex_status: v.optional(v.union(v.literal("draft"), v.literal("published"))),`);
|
|
1457
|
+
if (collection.versions?.drafts) {
|
|
1458
|
+
lines.push(` vex_version: v.optional(v.number()),`);
|
|
1459
|
+
lines.push(` vex_publishedAt: v.optional(v.number()),`);
|
|
1460
|
+
}
|
|
1461
|
+
lines.push("})");
|
|
1462
|
+
for (const i of indexes) {
|
|
1463
|
+
const fieldList = i.fields.map((f) => `"${f}"`).join(", ");
|
|
1464
|
+
lines.push(` .index("${i.name}", [${fieldList}])`);
|
|
1465
|
+
}
|
|
1466
|
+
for (const si of searchIndexes) {
|
|
1467
|
+
const filterList = si.filterFields.length > 0 ? `, filterFields: [${si.filterFields.map((f) => `"${f}"`).join(", ")}]` : "";
|
|
1468
|
+
lines.push(
|
|
1469
|
+
` .searchIndex("${si.name}", { searchField: "${si.searchField}"${filterList} })`
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
if (config.media && config.media.collections.length > 0) {
|
|
1474
|
+
lines.push("", "/**", " * MEDIA COLLECTIONS", " **/");
|
|
1475
|
+
for (const mediaCollection of config.media.collections) {
|
|
1476
|
+
const fields = [];
|
|
1477
|
+
const indexes = collectIndexes({ collection: mediaCollection });
|
|
1478
|
+
const searchIndexes = collectSearchIndexes({
|
|
1479
|
+
collection: mediaCollection
|
|
1480
|
+
});
|
|
1481
|
+
for (const [fieldName, field] of Object.entries(
|
|
1482
|
+
mediaCollection.fields
|
|
1483
|
+
)) {
|
|
1484
|
+
if (field.type === "ui") continue;
|
|
1485
|
+
if (fieldName === "storageId") {
|
|
1486
|
+
fields.push({
|
|
1487
|
+
name: fieldName,
|
|
1488
|
+
valueType: config.media.storageAdapter.storageIdValueType
|
|
1489
|
+
});
|
|
1490
|
+
} else {
|
|
1491
|
+
fields.push({
|
|
1492
|
+
name: fieldName,
|
|
1493
|
+
valueType: fieldToValueType({
|
|
1494
|
+
fieldName,
|
|
1495
|
+
field,
|
|
1496
|
+
collectionSlug: mediaCollection.slug
|
|
1497
|
+
})
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
lines.push(
|
|
1502
|
+
"",
|
|
1503
|
+
`export const ${mediaCollection.tableName ?? mediaCollection.slug} = defineTable({`
|
|
1504
|
+
);
|
|
1505
|
+
for (const f of fields) {
|
|
1506
|
+
lines.push(` ${f.name}: ${f.valueType},`);
|
|
1507
|
+
}
|
|
1508
|
+
lines.push("})");
|
|
1509
|
+
for (const i of indexes) {
|
|
1510
|
+
const fieldList = i.fields.map((f) => `"${f}"`).join(", ");
|
|
1511
|
+
lines.push(` .index("${i.name}", [${fieldList}])`);
|
|
1512
|
+
}
|
|
1513
|
+
for (const si of searchIndexes) {
|
|
1514
|
+
const filterList = si.filterFields.length > 0 ? `, filterFields: [${si.filterFields.map((f) => `"${f}"`).join(", ")}]` : "";
|
|
1515
|
+
lines.push(
|
|
1516
|
+
` .searchIndex("${si.name}", { searchField: "${si.searchField}"${filterList} })`
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
const unmergedAuthCollections = config.auth.collections.filter(
|
|
1522
|
+
(c) => !mergedAuthSlugs.has(c.slug)
|
|
1523
|
+
);
|
|
1524
|
+
if (unmergedAuthCollections.length > 0) {
|
|
1525
|
+
lines.push("", "/**", " * AUTH TABLES", " **/");
|
|
1526
|
+
}
|
|
1527
|
+
for (const authCollection of unmergedAuthCollections) {
|
|
1528
|
+
const indexes = collectIndexes({
|
|
1529
|
+
collection: authCollection
|
|
1530
|
+
});
|
|
1531
|
+
lines.push(
|
|
1532
|
+
"",
|
|
1533
|
+
`export const ${authCollection.tableName ?? authCollection.slug} = defineTable({`
|
|
1534
|
+
);
|
|
1535
|
+
for (const [name, field] of Object.entries(authCollection.fields)) {
|
|
1536
|
+
lines.push(
|
|
1537
|
+
` ${name}: ${fieldToValueType({
|
|
1538
|
+
field,
|
|
1539
|
+
collectionSlug: authCollection.slug,
|
|
1540
|
+
fieldName: name
|
|
1541
|
+
})},`
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
lines.push("})");
|
|
1545
|
+
for (const i of indexes) {
|
|
1546
|
+
const fieldList = i.fields.map((f) => `"${f}"`).join(", ");
|
|
1547
|
+
lines.push(` .index("${i.name}", [${fieldList}])`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
for (const global of config.globals) {
|
|
1551
|
+
const fields = [];
|
|
1552
|
+
const indexes = collectIndexes({ collection: global });
|
|
1553
|
+
for (const [fieldName, field] of Object.entries(global.fields)) {
|
|
1554
|
+
fields.push({
|
|
1555
|
+
name: fieldName,
|
|
1556
|
+
valueType: fieldToValueType({
|
|
1557
|
+
fieldName,
|
|
1558
|
+
field,
|
|
1559
|
+
collectionSlug: global.slug
|
|
1560
|
+
})
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
lines.push(
|
|
1564
|
+
"",
|
|
1565
|
+
`export const ${global.tableName ?? global.slug} = defineTable({`
|
|
1566
|
+
);
|
|
1567
|
+
for (const f of fields) {
|
|
1568
|
+
lines.push(` ${f.name}: ${f.valueType},`);
|
|
1569
|
+
}
|
|
1570
|
+
lines.push("})");
|
|
1571
|
+
for (const i of indexes) {
|
|
1572
|
+
const fieldList = i.fields.map((f) => `"${f}"`).join(", ");
|
|
1573
|
+
lines.push(` .index("${i.name}", [${fieldList}])`);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
{
|
|
1577
|
+
lines.push("", "/**", " * VEX SYSTEM TABLES", " **/");
|
|
1578
|
+
lines.push("");
|
|
1579
|
+
lines.push("export const vex_versions = defineTable({");
|
|
1580
|
+
lines.push(" collection: v.string(),");
|
|
1581
|
+
lines.push(" documentId: v.string(),");
|
|
1582
|
+
lines.push(" version: v.number(),");
|
|
1583
|
+
lines.push(` status: v.union(v.literal("draft"), v.literal("published"), v.literal("autosave"), v.literal("previewSnapshot")),`);
|
|
1584
|
+
lines.push(" snapshot: v.any(),");
|
|
1585
|
+
lines.push(" createdAt: v.number(),");
|
|
1586
|
+
lines.push(" createdBy: v.optional(v.string()),");
|
|
1587
|
+
lines.push(" isAutosave: v.boolean(),");
|
|
1588
|
+
lines.push(" restoredFrom: v.optional(v.number()),");
|
|
1589
|
+
lines.push("})");
|
|
1590
|
+
lines.push(` .index("by_document", ["collection", "documentId"])`);
|
|
1591
|
+
lines.push(` .index("by_document_version", ["collection", "documentId", "version"])`);
|
|
1592
|
+
lines.push(` .index("by_document_latest", ["collection", "documentId", "createdAt"])`);
|
|
1593
|
+
lines.push(` .index("by_document_status", ["collection", "documentId", "status"])`);
|
|
1594
|
+
lines.push(` .index("by_autosave", ["collection", "documentId", "isAutosave"])`);
|
|
1595
|
+
}
|
|
1596
|
+
return lines.join("\n") + "\n";
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// src/schema/extendTable.ts
|
|
1600
|
+
import { defineTable } from "convex/server";
|
|
1601
|
+
function extendTable(props) {
|
|
1602
|
+
const { validator } = props.table;
|
|
1603
|
+
let extended = defineTable({
|
|
1604
|
+
...validator.fields,
|
|
1605
|
+
...props.additionalFields
|
|
1606
|
+
});
|
|
1607
|
+
for (const idx of props.table[" indexes"]()) {
|
|
1608
|
+
extended = extended.index(
|
|
1609
|
+
idx.indexDescriptor,
|
|
1610
|
+
idx.fields
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
const source = props.table;
|
|
1614
|
+
for (const idx of source.searchIndexes ?? []) {
|
|
1615
|
+
extended = extended.searchIndex(idx.indexDescriptor, {
|
|
1616
|
+
searchField: idx.searchField,
|
|
1617
|
+
filterFields: idx.filterFields
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
for (const idx of source.vectorIndexes ?? []) {
|
|
1621
|
+
extended = extended.vectorIndex(idx.indexDescriptor, {
|
|
1622
|
+
vectorField: idx.vectorField,
|
|
1623
|
+
dimensions: idx.dimensions,
|
|
1624
|
+
filterFields: idx.filterFields
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
return extended;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/fields/media/columnDef.ts
|
|
1631
|
+
function uploadColumnDef(props) {
|
|
1632
|
+
return {
|
|
1633
|
+
accessorKey: props.fieldKey,
|
|
1634
|
+
header: (props.field.hasMany ? props.field.labels?.singular : props.field.label) ?? toTitleCase(props.fieldKey),
|
|
1635
|
+
meta: {
|
|
1636
|
+
type: "upload",
|
|
1637
|
+
to: props.field.to,
|
|
1638
|
+
noTruncate: true
|
|
1639
|
+
},
|
|
1640
|
+
cell: (info) => {
|
|
1641
|
+
const value = info.getValue();
|
|
1642
|
+
if (!value || typeof value !== "string") return "";
|
|
1643
|
+
return value;
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// src/columns/generateColumns.ts
|
|
1649
|
+
function generateColumns(props) {
|
|
1650
|
+
const { collection, auth } = props;
|
|
1651
|
+
const columns = [];
|
|
1652
|
+
const useAsTitle = collection.admin?.useAsTitle;
|
|
1653
|
+
const defaultColumns = collection.admin?.defaultColumns;
|
|
1654
|
+
const fields = collection.fields;
|
|
1655
|
+
const authFields = {};
|
|
1656
|
+
if (auth) {
|
|
1657
|
+
const authCollection = auth.collections.find(
|
|
1658
|
+
(c) => c.slug === collection.slug
|
|
1659
|
+
);
|
|
1660
|
+
if (authCollection) {
|
|
1661
|
+
for (const [k, v2] of Object.entries(authCollection.fields)) {
|
|
1662
|
+
authFields[k] = v2;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
if (defaultColumns) {
|
|
1667
|
+
for (const fieldKey of defaultColumns) {
|
|
1668
|
+
if (fieldKey === "_id") {
|
|
1669
|
+
columns.push({ accessorKey: "_id", header: "ID" });
|
|
1670
|
+
continue;
|
|
1671
|
+
}
|
|
1672
|
+
const field = fields[fieldKey] ?? authFields[fieldKey];
|
|
1673
|
+
if (!field) {
|
|
1674
|
+
columns.push({ accessorKey: fieldKey, header: toTitleCase(fieldKey) });
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
if (field.admin?.hidden) continue;
|
|
1678
|
+
if (field.type === "ui") continue;
|
|
1679
|
+
const col = buildColumnDef(fieldKey, field);
|
|
1680
|
+
if (useAsTitle && fieldKey === useAsTitle) {
|
|
1681
|
+
col.meta = { ...col.meta, isTitle: true };
|
|
1682
|
+
}
|
|
1683
|
+
if (field.admin?.components?.Cell) {
|
|
1684
|
+
col.meta = {
|
|
1685
|
+
...col.meta,
|
|
1686
|
+
customCell: field.admin.components.Cell,
|
|
1687
|
+
fieldDef: field
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
columns.push(col);
|
|
1691
|
+
}
|
|
1692
|
+
} else {
|
|
1693
|
+
columns.push({ accessorKey: "_id", header: "ID" });
|
|
1694
|
+
const allFieldKeys = new Set(Object.keys(fields));
|
|
1695
|
+
for (const k of Object.keys(authFields)) {
|
|
1696
|
+
allFieldKeys.add(k);
|
|
1697
|
+
}
|
|
1698
|
+
for (const fieldKey of allFieldKeys) {
|
|
1699
|
+
const field = fields[fieldKey] ?? authFields[fieldKey];
|
|
1700
|
+
if (field.admin?.hidden) continue;
|
|
1701
|
+
if (field.type === "ui") continue;
|
|
1702
|
+
const col = buildColumnDef(fieldKey, field);
|
|
1703
|
+
if (useAsTitle && fieldKey === useAsTitle) {
|
|
1704
|
+
col.meta = { ...col.meta, isTitle: true };
|
|
1705
|
+
}
|
|
1706
|
+
if (field.admin?.components?.Cell) {
|
|
1707
|
+
col.meta = {
|
|
1708
|
+
...col.meta,
|
|
1709
|
+
customCell: field.admin.components.Cell,
|
|
1710
|
+
fieldDef: field
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
columns.push(col);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
return columns;
|
|
1717
|
+
}
|
|
1718
|
+
function buildColumnDef(fieldKey, field) {
|
|
1719
|
+
switch (field.type) {
|
|
1720
|
+
case "text":
|
|
1721
|
+
return textColumnDef({ fieldKey, field });
|
|
1722
|
+
case "number":
|
|
1723
|
+
return numberColumnDef({ fieldKey, field });
|
|
1724
|
+
case "checkbox":
|
|
1725
|
+
return checkboxColumnDef({ fieldKey, field });
|
|
1726
|
+
case "select":
|
|
1727
|
+
return selectColumnDef({ fieldKey, field });
|
|
1728
|
+
case "date":
|
|
1729
|
+
return dateColumnDef({ fieldKey, field });
|
|
1730
|
+
case "imageUrl":
|
|
1731
|
+
return imageUrlColumnDef({ fieldKey, field });
|
|
1732
|
+
case "relationship":
|
|
1733
|
+
return relationshipColumnDef({ fieldKey, field });
|
|
1734
|
+
case "json":
|
|
1735
|
+
return jsonColumnDef({ fieldKey, field });
|
|
1736
|
+
case "richtext":
|
|
1737
|
+
return richtextColumnDef({ fieldKey, field });
|
|
1738
|
+
case "array":
|
|
1739
|
+
return arrayColumnDef({ fieldKey, field });
|
|
1740
|
+
case "upload":
|
|
1741
|
+
return uploadColumnDef({ fieldKey, field });
|
|
1742
|
+
case "blocks":
|
|
1743
|
+
return blocksColumnDef({ fieldKey, field });
|
|
1744
|
+
default:
|
|
1745
|
+
return {
|
|
1746
|
+
accessorKey: fieldKey,
|
|
1747
|
+
header: toTitleCase(fieldKey)
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// src/formSchema/generateFormSchema.ts
|
|
1753
|
+
import { z } from "zod";
|
|
1754
|
+
function generateFormSchema(props) {
|
|
1755
|
+
const shape = {};
|
|
1756
|
+
for (const [fieldName, field] of Object.entries(props.fields)) {
|
|
1757
|
+
if (field.admin?.hidden) continue;
|
|
1758
|
+
if (field.type === "ui") continue;
|
|
1759
|
+
let validator = fieldMetaToZod({ field });
|
|
1760
|
+
if (!field.required) {
|
|
1761
|
+
validator = validator.optional();
|
|
1762
|
+
}
|
|
1763
|
+
shape[fieldName] = validator;
|
|
1764
|
+
}
|
|
1765
|
+
return z.object(shape);
|
|
1766
|
+
}
|
|
1767
|
+
function fieldMetaToZod(props) {
|
|
1768
|
+
switch (props.field.type) {
|
|
1769
|
+
case "text": {
|
|
1770
|
+
let schema = z.string();
|
|
1771
|
+
if (props.field.minLength != null) schema = schema.min(props.field.minLength);
|
|
1772
|
+
if (props.field.maxLength != null) schema = schema.max(props.field.maxLength);
|
|
1773
|
+
return schema;
|
|
1774
|
+
}
|
|
1775
|
+
case "number": {
|
|
1776
|
+
let schema = z.number();
|
|
1777
|
+
if (props.field.min != null) schema = schema.min(props.field.min);
|
|
1778
|
+
if (props.field.max != null) schema = schema.max(props.field.max);
|
|
1779
|
+
return schema;
|
|
1780
|
+
}
|
|
1781
|
+
case "checkbox":
|
|
1782
|
+
return z.boolean();
|
|
1783
|
+
case "select": {
|
|
1784
|
+
const values = props.field.options.map((o) => o.value);
|
|
1785
|
+
if (values.length === 0) return z.string();
|
|
1786
|
+
const enumSchema = z.enum(values);
|
|
1787
|
+
if (props.field.hasMany) {
|
|
1788
|
+
return z.array(enumSchema);
|
|
1789
|
+
}
|
|
1790
|
+
return enumSchema;
|
|
1791
|
+
}
|
|
1792
|
+
case "date":
|
|
1793
|
+
return z.number();
|
|
1794
|
+
case "imageUrl":
|
|
1795
|
+
return z.string().url().or(z.literal(""));
|
|
1796
|
+
case "relationship": {
|
|
1797
|
+
if (props.field.hasMany) {
|
|
1798
|
+
return z.array(z.string());
|
|
1799
|
+
}
|
|
1800
|
+
return z.string();
|
|
1801
|
+
}
|
|
1802
|
+
case "upload": {
|
|
1803
|
+
if (props.field.hasMany) {
|
|
1804
|
+
return z.array(z.string());
|
|
1805
|
+
}
|
|
1806
|
+
return z.string();
|
|
1807
|
+
}
|
|
1808
|
+
case "json":
|
|
1809
|
+
return z.any();
|
|
1810
|
+
case "richtext":
|
|
1811
|
+
return z.any();
|
|
1812
|
+
case "array": {
|
|
1813
|
+
let schema = z.array(fieldMetaToZod({ field: props.field.field }));
|
|
1814
|
+
if (props.field.min != null) schema = schema.min(props.field.min);
|
|
1815
|
+
if (props.field.max != null) schema = schema.max(props.field.max);
|
|
1816
|
+
return schema;
|
|
1817
|
+
}
|
|
1818
|
+
case "blocks": {
|
|
1819
|
+
const blockSchemas = props.field.blocks.map((blockDef) => {
|
|
1820
|
+
const shape = {
|
|
1821
|
+
blockType: z.literal(blockDef.slug),
|
|
1822
|
+
blockName: z.string().optional(),
|
|
1823
|
+
_key: z.string()
|
|
1824
|
+
};
|
|
1825
|
+
for (const [fieldName, field] of Object.entries(blockDef.fields)) {
|
|
1826
|
+
let validator = fieldMetaToZod({ field });
|
|
1827
|
+
if (!field.required) {
|
|
1828
|
+
validator = validator.optional();
|
|
1829
|
+
}
|
|
1830
|
+
shape[fieldName] = validator;
|
|
1831
|
+
}
|
|
1832
|
+
return z.object(shape);
|
|
1833
|
+
});
|
|
1834
|
+
if (blockSchemas.length === 0) {
|
|
1835
|
+
let schema2 = z.array(z.any());
|
|
1836
|
+
if (props.field.min != null) schema2 = schema2.min(props.field.min);
|
|
1837
|
+
if (props.field.max != null) schema2 = schema2.max(props.field.max);
|
|
1838
|
+
return schema2;
|
|
1839
|
+
}
|
|
1840
|
+
const union = blockSchemas.length === 1 ? blockSchemas[0] : z.discriminatedUnion(
|
|
1841
|
+
"blockType",
|
|
1842
|
+
blockSchemas
|
|
1843
|
+
);
|
|
1844
|
+
let schema = z.array(union);
|
|
1845
|
+
if (props.field.min != null) schema = schema.min(props.field.min);
|
|
1846
|
+
if (props.field.max != null) schema = schema.max(props.field.max);
|
|
1847
|
+
return schema;
|
|
1848
|
+
}
|
|
1849
|
+
default:
|
|
1850
|
+
return z.any();
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// src/formSchema/generateFormDefaultValues.ts
|
|
1855
|
+
function getFormDefaultValue(props) {
|
|
1856
|
+
switch (props.field.type) {
|
|
1857
|
+
case "text":
|
|
1858
|
+
return props.field.defaultValue ?? "";
|
|
1859
|
+
case "number":
|
|
1860
|
+
return props.field.defaultValue ?? 0;
|
|
1861
|
+
case "checkbox":
|
|
1862
|
+
return props.field.defaultValue ?? false;
|
|
1863
|
+
case "select":
|
|
1864
|
+
if (props.field.hasMany) {
|
|
1865
|
+
return props.field.defaultValue ? [props.field.defaultValue] : [];
|
|
1866
|
+
}
|
|
1867
|
+
return props.field.defaultValue ?? "";
|
|
1868
|
+
case "date":
|
|
1869
|
+
return props.field.defaultValue ?? 0;
|
|
1870
|
+
case "imageUrl":
|
|
1871
|
+
return props.field.defaultValue ?? "";
|
|
1872
|
+
case "relationship":
|
|
1873
|
+
return props.field.hasMany ? [] : "";
|
|
1874
|
+
case "upload":
|
|
1875
|
+
return props.field.hasMany ? [] : "";
|
|
1876
|
+
case "json":
|
|
1877
|
+
return {};
|
|
1878
|
+
case "richtext":
|
|
1879
|
+
return [];
|
|
1880
|
+
case "array":
|
|
1881
|
+
return [];
|
|
1882
|
+
case "blocks":
|
|
1883
|
+
return [];
|
|
1884
|
+
case "ui":
|
|
1885
|
+
return void 0;
|
|
1886
|
+
default:
|
|
1887
|
+
return void 0;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
function generateFormDefaultValues(props) {
|
|
1891
|
+
const result = {};
|
|
1892
|
+
for (const [fieldName, field] of Object.entries(props.fields)) {
|
|
1893
|
+
if (field.admin?.hidden) continue;
|
|
1894
|
+
if (field.type === "ui") continue;
|
|
1895
|
+
result[fieldName] = getFormDefaultValue({ field });
|
|
1896
|
+
}
|
|
1897
|
+
return result;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// src/fields/ui/config.ts
|
|
1901
|
+
function ui(props) {
|
|
1902
|
+
return {
|
|
1903
|
+
type: "ui",
|
|
1904
|
+
label: props.label,
|
|
1905
|
+
description: props.description,
|
|
1906
|
+
admin: props.admin
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// src/types/fields.ts
|
|
1911
|
+
var RESERVED_BLOCK_FIELD_NAMES = ["blockType", "blockName", "_key"];
|
|
1912
|
+
|
|
1913
|
+
// src/blocks/defineBlock.ts
|
|
1914
|
+
function defineBlock(props) {
|
|
1915
|
+
if (!props.slug || !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(props.slug)) {
|
|
1916
|
+
throw new VexBlockValidationError(
|
|
1917
|
+
props.slug || "(empty)",
|
|
1918
|
+
`Invalid block slug "${props.slug}". Slugs must start with a letter and contain only letters, numbers, hyphens, and underscores.`
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
for (const fieldName of Object.keys(props.fields)) {
|
|
1922
|
+
if (RESERVED_BLOCK_FIELD_NAMES.includes(fieldName)) {
|
|
1923
|
+
throw new VexBlockValidationError(
|
|
1924
|
+
props.slug,
|
|
1925
|
+
`Field name "${fieldName}" is reserved in block definitions. Reserved names: ${RESERVED_BLOCK_FIELD_NAMES.join(", ")}`
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
return {
|
|
1930
|
+
slug: props.slug,
|
|
1931
|
+
label: props.label,
|
|
1932
|
+
fields: props.fields,
|
|
1933
|
+
admin: props.admin,
|
|
1934
|
+
interfaceName: props.interfaceName
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// src/typeGen/slugToInterfaceName.ts
|
|
1939
|
+
function slugToInterfaceName(props) {
|
|
1940
|
+
return props.slug.replace(/[-_]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").split(/\s+/).filter(Boolean).map((seg) => seg.charAt(0).toUpperCase() + seg.slice(1).toLowerCase()).join("");
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// src/typeGen/fieldToTypeString.ts
|
|
1944
|
+
function fieldToTypeString(props) {
|
|
1945
|
+
switch (props.field.type) {
|
|
1946
|
+
case "text":
|
|
1947
|
+
return "string";
|
|
1948
|
+
case "number":
|
|
1949
|
+
return "number";
|
|
1950
|
+
case "checkbox":
|
|
1951
|
+
return "boolean";
|
|
1952
|
+
case "date":
|
|
1953
|
+
return "number";
|
|
1954
|
+
case "imageUrl":
|
|
1955
|
+
return "string";
|
|
1956
|
+
case "json":
|
|
1957
|
+
return "Record<string, unknown>";
|
|
1958
|
+
case "richtext":
|
|
1959
|
+
return "any";
|
|
1960
|
+
case "ui":
|
|
1961
|
+
return "never";
|
|
1962
|
+
case "select": {
|
|
1963
|
+
const values = props.field.options.map((o) => o.value);
|
|
1964
|
+
if (values.length === 0) return "string";
|
|
1965
|
+
const union = values.map((v2) => `'${v2}'`).join(" | ");
|
|
1966
|
+
if (props.field.hasMany) {
|
|
1967
|
+
return `(${union})[]`;
|
|
1968
|
+
}
|
|
1969
|
+
return union;
|
|
1970
|
+
}
|
|
1971
|
+
case "relationship": {
|
|
1972
|
+
const idType = `Id<'${props.field.to}'>`;
|
|
1973
|
+
return props.field.hasMany ? `${idType}[]` : idType;
|
|
1974
|
+
}
|
|
1975
|
+
case "upload": {
|
|
1976
|
+
const idType = `Id<'${props.field.to}'>`;
|
|
1977
|
+
return props.field.hasMany ? `${idType}[]` : idType;
|
|
1978
|
+
}
|
|
1979
|
+
case "array": {
|
|
1980
|
+
const inner = fieldToTypeString({
|
|
1981
|
+
field: props.field.field,
|
|
1982
|
+
blockInterfaceNames: props.blockInterfaceNames
|
|
1983
|
+
});
|
|
1984
|
+
const needsParens = inner.includes("|");
|
|
1985
|
+
return needsParens ? `(${inner})[]` : `${inner}[]`;
|
|
1986
|
+
}
|
|
1987
|
+
case "blocks": {
|
|
1988
|
+
const names = props.field.blocks.map((b) => {
|
|
1989
|
+
if (props.blockInterfaceNames?.has(b.slug)) {
|
|
1990
|
+
return props.blockInterfaceNames.get(b.slug);
|
|
1991
|
+
}
|
|
1992
|
+
return b.interfaceName ?? slugToInterfaceName({ slug: b.slug });
|
|
1993
|
+
});
|
|
1994
|
+
if (names.length === 0) return "unknown[]";
|
|
1995
|
+
if (names.length === 1) return `${names[0]}[]`;
|
|
1996
|
+
return `(${names.join(" | ")})[]`;
|
|
1997
|
+
}
|
|
1998
|
+
default:
|
|
1999
|
+
return "unknown";
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// src/typeGen/generateVexTypes.ts
|
|
2004
|
+
function generateVexTypes(props) {
|
|
2005
|
+
const config = props.config;
|
|
2006
|
+
const parts = [];
|
|
2007
|
+
const blocksBySlug = /* @__PURE__ */ new Map();
|
|
2008
|
+
const blockInterfaceNames = /* @__PURE__ */ new Map();
|
|
2009
|
+
function collectBlocks(fields) {
|
|
2010
|
+
for (const field of Object.values(fields)) {
|
|
2011
|
+
if (field.type === "blocks") {
|
|
2012
|
+
for (const block of field.blocks) {
|
|
2013
|
+
if (!blocksBySlug.has(block.slug)) {
|
|
2014
|
+
blocksBySlug.set(block.slug, block);
|
|
2015
|
+
collectBlocks(block.fields);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
for (const col of config.collections) {
|
|
2022
|
+
collectBlocks(col.fields);
|
|
2023
|
+
}
|
|
2024
|
+
if (config.media?.collections) {
|
|
2025
|
+
for (const col of config.media.collections) {
|
|
2026
|
+
collectBlocks(col.fields);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
for (const g of config.globals) {
|
|
2030
|
+
collectBlocks(g.fields);
|
|
2031
|
+
}
|
|
2032
|
+
for (const [slug, block] of blocksBySlug) {
|
|
2033
|
+
blockInterfaceNames.set(
|
|
2034
|
+
slug,
|
|
2035
|
+
block.interfaceName ?? slugToInterfaceName({ slug })
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
const allNames = /* @__PURE__ */ new Map();
|
|
2039
|
+
function registerName(name, source) {
|
|
2040
|
+
if (allNames.has(name)) {
|
|
2041
|
+
throw new VexError(
|
|
2042
|
+
`Duplicate interface name "${name}" \u2014 used by ${allNames.get(name)} and ${source}. Set a unique \`interfaceName\` on one of them.`
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
allNames.set(name, source);
|
|
2046
|
+
}
|
|
2047
|
+
const collectionNames = /* @__PURE__ */ new Map();
|
|
2048
|
+
for (const col of config.collections) {
|
|
2049
|
+
const name = col.interfaceName ?? slugToInterfaceName({ slug: col.slug });
|
|
2050
|
+
registerName(name, `collection "${col.slug}"`);
|
|
2051
|
+
collectionNames.set(col.slug, name);
|
|
2052
|
+
}
|
|
2053
|
+
if (config.media?.collections) {
|
|
2054
|
+
for (const col of config.media.collections) {
|
|
2055
|
+
const name = col.interfaceName ?? slugToInterfaceName({ slug: col.slug });
|
|
2056
|
+
registerName(name, `media collection "${col.slug}"`);
|
|
2057
|
+
collectionNames.set(col.slug, name);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
const authCollectionMap = new Map(
|
|
2061
|
+
config.auth.collections.map((c) => [c.slug, c])
|
|
2062
|
+
);
|
|
2063
|
+
const userCollectionSlugs = new Set(config.collections.map((c) => c.slug));
|
|
2064
|
+
const mediaSlugs = new Set(
|
|
2065
|
+
(config.media?.collections ?? []).map((c) => c.slug)
|
|
2066
|
+
);
|
|
2067
|
+
for (const authCol of config.auth.collections) {
|
|
2068
|
+
if (!userCollectionSlugs.has(authCol.slug) && !mediaSlugs.has(authCol.slug)) {
|
|
2069
|
+
const name = authCol.interfaceName ?? slugToInterfaceName({ slug: authCol.slug });
|
|
2070
|
+
registerName(name, `auth collection "${authCol.slug}"`);
|
|
2071
|
+
collectionNames.set(authCol.slug, name);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
const globalNames = /* @__PURE__ */ new Map();
|
|
2075
|
+
for (const g of config.globals) {
|
|
2076
|
+
const name = g.interfaceName ?? slugToInterfaceName({ slug: g.slug });
|
|
2077
|
+
registerName(name, `global "${g.slug}"`);
|
|
2078
|
+
globalNames.set(g.slug, name);
|
|
2079
|
+
}
|
|
2080
|
+
for (const [slug, name] of blockInterfaceNames) {
|
|
2081
|
+
registerName(name, `block "${slug}"`);
|
|
2082
|
+
}
|
|
2083
|
+
let needsIdImport = false;
|
|
2084
|
+
function checkForIdFields(fields) {
|
|
2085
|
+
for (const field of Object.values(fields)) {
|
|
2086
|
+
if (field.type === "relationship" || field.type === "upload") {
|
|
2087
|
+
needsIdImport = true;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
for (const col of config.collections) {
|
|
2092
|
+
checkForIdFields(col.fields);
|
|
2093
|
+
}
|
|
2094
|
+
if (config.media?.collections) {
|
|
2095
|
+
for (const col of config.media.collections) {
|
|
2096
|
+
checkForIdFields(col.fields);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
for (const g of config.globals) {
|
|
2100
|
+
checkForIdFields(g.fields);
|
|
2101
|
+
}
|
|
2102
|
+
for (const block of blocksBySlug.values()) {
|
|
2103
|
+
checkForIdFields(block.fields);
|
|
2104
|
+
}
|
|
2105
|
+
if (config.collections.length > 0 || (config.media?.collections?.length ?? 0) > 0 || config.globals.length > 0) {
|
|
2106
|
+
needsIdImport = true;
|
|
2107
|
+
}
|
|
2108
|
+
parts.push("// \u26A0\uFE0F AUTO-GENERATED BY VEX CMS \u2014 DO NOT EDIT \u26A0\uFE0F");
|
|
2109
|
+
parts.push("");
|
|
2110
|
+
if (needsIdImport) {
|
|
2111
|
+
parts.push("import type { Id } from './_generated/dataModel';");
|
|
2112
|
+
parts.push("");
|
|
2113
|
+
}
|
|
2114
|
+
const sortedBlockSlugs = [...blocksBySlug.keys()].sort();
|
|
2115
|
+
for (const slug of sortedBlockSlugs) {
|
|
2116
|
+
const block = blocksBySlug.get(slug);
|
|
2117
|
+
const name = blockInterfaceNames.get(slug);
|
|
2118
|
+
parts.push(generateBlockInterface({ block, name, blockInterfaceNames }));
|
|
2119
|
+
parts.push("");
|
|
2120
|
+
}
|
|
2121
|
+
for (const col of config.collections) {
|
|
2122
|
+
const name = collectionNames.get(col.slug);
|
|
2123
|
+
const authCol = authCollectionMap.get(col.slug);
|
|
2124
|
+
let fields;
|
|
2125
|
+
if (authCol) {
|
|
2126
|
+
const merged = mergeAuthCollectionWithUserCollection({
|
|
2127
|
+
authCollection: authCol,
|
|
2128
|
+
userCollection: col
|
|
2129
|
+
});
|
|
2130
|
+
fields = merged.fields;
|
|
2131
|
+
} else {
|
|
2132
|
+
fields = col.fields;
|
|
2133
|
+
}
|
|
2134
|
+
const isVersioned = !!col.versions?.drafts;
|
|
2135
|
+
parts.push(
|
|
2136
|
+
generateCollectionInterface({
|
|
2137
|
+
name,
|
|
2138
|
+
slug: col.slug,
|
|
2139
|
+
fields,
|
|
2140
|
+
isVersioned,
|
|
2141
|
+
blockInterfaceNames
|
|
2142
|
+
})
|
|
2143
|
+
);
|
|
2144
|
+
parts.push("");
|
|
2145
|
+
}
|
|
2146
|
+
for (const authCol of config.auth.collections) {
|
|
2147
|
+
if (userCollectionSlugs.has(authCol.slug) || mediaSlugs.has(authCol.slug)) continue;
|
|
2148
|
+
const name = collectionNames.get(authCol.slug);
|
|
2149
|
+
parts.push(
|
|
2150
|
+
generateCollectionInterface({
|
|
2151
|
+
name,
|
|
2152
|
+
slug: authCol.slug,
|
|
2153
|
+
fields: authCol.fields,
|
|
2154
|
+
isVersioned: false,
|
|
2155
|
+
blockInterfaceNames
|
|
2156
|
+
})
|
|
2157
|
+
);
|
|
2158
|
+
parts.push("");
|
|
2159
|
+
}
|
|
2160
|
+
if (config.media?.collections) {
|
|
2161
|
+
for (const col of config.media.collections) {
|
|
2162
|
+
const name = collectionNames.get(col.slug);
|
|
2163
|
+
parts.push(
|
|
2164
|
+
generateMediaCollectionInterface({
|
|
2165
|
+
name,
|
|
2166
|
+
slug: col.slug,
|
|
2167
|
+
userFields: col.fields,
|
|
2168
|
+
blockInterfaceNames
|
|
2169
|
+
})
|
|
2170
|
+
);
|
|
2171
|
+
parts.push("");
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
for (const g of config.globals) {
|
|
2175
|
+
const name = globalNames.get(g.slug);
|
|
2176
|
+
parts.push(
|
|
2177
|
+
generateGlobalInterface({
|
|
2178
|
+
name,
|
|
2179
|
+
slug: g.slug,
|
|
2180
|
+
fields: g.fields,
|
|
2181
|
+
blockInterfaceNames
|
|
2182
|
+
})
|
|
2183
|
+
);
|
|
2184
|
+
parts.push("");
|
|
2185
|
+
}
|
|
2186
|
+
if (collectionNames.size > 0) {
|
|
2187
|
+
const entries = [...collectionNames.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([slug, name]) => ` ${slug}: ${name};`).join("\n");
|
|
2188
|
+
parts.push(`export interface VexCollectionTypes {
|
|
2189
|
+
${entries}
|
|
2190
|
+
}`);
|
|
2191
|
+
parts.push("");
|
|
2192
|
+
}
|
|
2193
|
+
if (globalNames.size > 0) {
|
|
2194
|
+
const entries = [...globalNames.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([slug, name]) => ` ${slug}: ${name};`).join("\n");
|
|
2195
|
+
parts.push(`export interface VexGlobalTypes {
|
|
2196
|
+
${entries}
|
|
2197
|
+
}`);
|
|
2198
|
+
parts.push("");
|
|
2199
|
+
}
|
|
2200
|
+
return parts.join("\n");
|
|
2201
|
+
}
|
|
2202
|
+
function generateBlockInterface(props) {
|
|
2203
|
+
const lines = [];
|
|
2204
|
+
lines.push(`export interface ${props.name} {`);
|
|
2205
|
+
lines.push(` blockType: '${props.block.slug}';`);
|
|
2206
|
+
lines.push(` blockName?: string;`);
|
|
2207
|
+
lines.push(` _key: string;`);
|
|
2208
|
+
for (const [fieldName, field] of Object.entries(props.block.fields)) {
|
|
2209
|
+
const f = field;
|
|
2210
|
+
if (f.type === "ui") continue;
|
|
2211
|
+
const label = f.label;
|
|
2212
|
+
if (label) lines.push(` /** ${label} */`);
|
|
2213
|
+
const optional = f.required ? "" : "?";
|
|
2214
|
+
const typeStr = fieldToTypeString({
|
|
2215
|
+
field: f,
|
|
2216
|
+
blockInterfaceNames: props.blockInterfaceNames
|
|
2217
|
+
});
|
|
2218
|
+
lines.push(` ${fieldName}${optional}: ${typeStr};`);
|
|
2219
|
+
}
|
|
2220
|
+
lines.push("}");
|
|
2221
|
+
return lines.join("\n");
|
|
2222
|
+
}
|
|
2223
|
+
function generateCollectionInterface(props) {
|
|
2224
|
+
const lines = [];
|
|
2225
|
+
lines.push(`export interface ${props.name} {`);
|
|
2226
|
+
lines.push(` _id: Id<'${props.slug}'>;`);
|
|
2227
|
+
lines.push(` _creationTime: number;`);
|
|
2228
|
+
if (props.isVersioned) {
|
|
2229
|
+
lines.push(` vex_status?: 'draft' | 'published';`);
|
|
2230
|
+
lines.push(` vex_version?: number;`);
|
|
2231
|
+
lines.push(` vex_publishedAt?: number;`);
|
|
2232
|
+
}
|
|
2233
|
+
for (const [fieldName, field] of Object.entries(props.fields)) {
|
|
2234
|
+
const f = field;
|
|
2235
|
+
if (f.type === "ui") continue;
|
|
2236
|
+
const label = f.label;
|
|
2237
|
+
if (label) lines.push(` /** ${label} */`);
|
|
2238
|
+
const optional = f.required ? "" : "?";
|
|
2239
|
+
const typeStr = fieldToTypeString({
|
|
2240
|
+
field: f,
|
|
2241
|
+
blockInterfaceNames: props.blockInterfaceNames
|
|
2242
|
+
});
|
|
2243
|
+
lines.push(` ${fieldName}${optional}: ${typeStr};`);
|
|
2244
|
+
}
|
|
2245
|
+
lines.push("}");
|
|
2246
|
+
return lines.join("\n");
|
|
2247
|
+
}
|
|
2248
|
+
function generateMediaCollectionInterface(props) {
|
|
2249
|
+
const lines = [];
|
|
2250
|
+
lines.push(`export interface ${props.name} {`);
|
|
2251
|
+
lines.push(` _id: Id<'${props.slug}'>;`);
|
|
2252
|
+
lines.push(` _creationTime: number;`);
|
|
2253
|
+
lines.push(` storageId: string;`);
|
|
2254
|
+
lines.push(` filename: string;`);
|
|
2255
|
+
lines.push(` mimeType: string;`);
|
|
2256
|
+
lines.push(` size: number;`);
|
|
2257
|
+
lines.push(` url?: string;`);
|
|
2258
|
+
lines.push(` width?: number;`);
|
|
2259
|
+
lines.push(` height?: number;`);
|
|
2260
|
+
const lockedSet = /* @__PURE__ */ new Set([...LOCKED_MEDIA_FIELDS, ...OVERRIDABLE_MEDIA_FIELDS]);
|
|
2261
|
+
for (const [fieldName, field] of Object.entries(props.userFields)) {
|
|
2262
|
+
if (lockedSet.has(fieldName)) continue;
|
|
2263
|
+
const f = field;
|
|
2264
|
+
if (f.type === "ui") continue;
|
|
2265
|
+
const label = f.label;
|
|
2266
|
+
if (label) lines.push(` /** ${label} */`);
|
|
2267
|
+
const optional = f.required ? "" : "?";
|
|
2268
|
+
const typeStr = fieldToTypeString({
|
|
2269
|
+
field: f,
|
|
2270
|
+
blockInterfaceNames: props.blockInterfaceNames
|
|
2271
|
+
});
|
|
2272
|
+
lines.push(` ${fieldName}${optional}: ${typeStr};`);
|
|
2273
|
+
}
|
|
2274
|
+
lines.push("}");
|
|
2275
|
+
return lines.join("\n");
|
|
2276
|
+
}
|
|
2277
|
+
function generateGlobalInterface(props) {
|
|
2278
|
+
const lines = [];
|
|
2279
|
+
lines.push(`export interface ${props.name} {`);
|
|
2280
|
+
lines.push(` _id: Id<'vex_globals'>;`);
|
|
2281
|
+
lines.push(` _creationTime: number;`);
|
|
2282
|
+
lines.push(` vexGlobalSlug: '${props.slug}';`);
|
|
2283
|
+
for (const [fieldName, field] of Object.entries(props.fields)) {
|
|
2284
|
+
const f = field;
|
|
2285
|
+
if (f.type === "ui") continue;
|
|
2286
|
+
const label = f.label;
|
|
2287
|
+
if (label) lines.push(` /** ${label} */`);
|
|
2288
|
+
const optional = f.required ? "" : "?";
|
|
2289
|
+
const typeStr = fieldToTypeString({
|
|
2290
|
+
field: f,
|
|
2291
|
+
blockInterfaceNames: props.blockInterfaceNames
|
|
2292
|
+
});
|
|
2293
|
+
lines.push(` ${fieldName}${optional}: ${typeStr};`);
|
|
2294
|
+
}
|
|
2295
|
+
lines.push("}");
|
|
2296
|
+
return lines.join("\n");
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
// src/versioning/constants.ts
|
|
2300
|
+
var VERSION_SYSTEM_FIELDS = [
|
|
2301
|
+
"vex_status",
|
|
2302
|
+
"vex_version",
|
|
2303
|
+
"vex_publishedAt"
|
|
2304
|
+
];
|
|
2305
|
+
var ALL_SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
2306
|
+
"_id",
|
|
2307
|
+
"_creationTime",
|
|
2308
|
+
...VERSION_SYSTEM_FIELDS
|
|
2309
|
+
]);
|
|
2310
|
+
var DEFAULT_MAX_VERSIONS_PER_DOC = 100;
|
|
2311
|
+
var DEFAULT_AUTOSAVE_INTERVAL = 2e3;
|
|
2312
|
+
|
|
2313
|
+
// src/versioning/extractUserFields.ts
|
|
2314
|
+
function extractUserFields(props) {
|
|
2315
|
+
const result = {};
|
|
2316
|
+
for (const [key, value] of Object.entries(props.document)) {
|
|
2317
|
+
if (!ALL_SYSTEM_FIELDS.has(key)) {
|
|
2318
|
+
result[key] = value;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
return result;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
// src/livePreview/resolvePreviewURL.ts
|
|
2325
|
+
function resolvePreviewURL(props) {
|
|
2326
|
+
if (typeof props.config.url === "string") {
|
|
2327
|
+
return props.config.url;
|
|
2328
|
+
}
|
|
2329
|
+
try {
|
|
2330
|
+
const result = props.config.url(props.doc);
|
|
2331
|
+
if (!result) {
|
|
2332
|
+
throw new Error(
|
|
2333
|
+
`Live preview URL resolved to empty string for document ${props.doc._id}`
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
return result;
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
if (props.fallbackURL !== void 0) {
|
|
2339
|
+
return props.fallbackURL;
|
|
2340
|
+
}
|
|
2341
|
+
throw error;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// src/livePreview/shouldReloadURL.ts
|
|
2346
|
+
function shouldReloadURL(props) {
|
|
2347
|
+
if (props.config.reloadOnFields === void 0) {
|
|
2348
|
+
return true;
|
|
2349
|
+
}
|
|
2350
|
+
if (props.config.reloadOnFields.length === 0) {
|
|
2351
|
+
return false;
|
|
2352
|
+
}
|
|
2353
|
+
return props.changedFields.some(
|
|
2354
|
+
(field) => props.config.reloadOnFields.includes(field)
|
|
2355
|
+
);
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// src/livePreview/constants.ts
|
|
2359
|
+
var DEFAULT_BREAKPOINTS = [
|
|
2360
|
+
{ label: "Mobile", width: 375, height: 667, icon: "smartphone" },
|
|
2361
|
+
{ label: "Tablet", width: 768, height: 1024, icon: "tablet" },
|
|
2362
|
+
{ label: "Laptop", width: 1280, height: 800, icon: "laptop" },
|
|
2363
|
+
{ label: "Desktop", width: 1920, height: 1080, icon: "monitor" }
|
|
2364
|
+
];
|
|
2365
|
+
var PREVIEW_SNAPSHOT_DEBOUNCE_MS = 500;
|
|
2366
|
+
|
|
2367
|
+
// src/convex/previewSnapshot.ts
|
|
2368
|
+
async function upsertPreviewSnapshot(props) {
|
|
2369
|
+
const existing = await props.ctx.db.query("vex_versions").withIndex(
|
|
2370
|
+
"by_document_status",
|
|
2371
|
+
(q) => q.eq("collection", props.collection).eq("documentId", props.documentId).eq("status", "previewSnapshot")
|
|
2372
|
+
).first();
|
|
2373
|
+
if (existing) {
|
|
2374
|
+
await props.ctx.db.patch(existing._id, {
|
|
2375
|
+
snapshot: props.snapshot,
|
|
2376
|
+
createdAt: Date.now()
|
|
2377
|
+
});
|
|
2378
|
+
} else {
|
|
2379
|
+
await props.ctx.db.insert("vex_versions", {
|
|
2380
|
+
collection: props.collection,
|
|
2381
|
+
documentId: props.documentId,
|
|
2382
|
+
version: 0,
|
|
2383
|
+
status: "previewSnapshot",
|
|
2384
|
+
snapshot: props.snapshot,
|
|
2385
|
+
createdAt: Date.now(),
|
|
2386
|
+
createdBy: void 0,
|
|
2387
|
+
isAutosave: false,
|
|
2388
|
+
restoredFrom: void 0
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
async function deletePreviewSnapshot(props) {
|
|
2393
|
+
const entries = await props.ctx.db.query("vex_versions").withIndex(
|
|
2394
|
+
"by_document_status",
|
|
2395
|
+
(q) => q.eq("collection", props.collection).eq("documentId", props.documentId).eq("status", "previewSnapshot")
|
|
2396
|
+
).collect();
|
|
2397
|
+
for (const entry of entries) {
|
|
2398
|
+
await props.ctx.db.delete(entry._id);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
async function getPreviewSnapshot(props) {
|
|
2402
|
+
const entry = await props.ctx.db.query("vex_versions").withIndex(
|
|
2403
|
+
"by_document_status",
|
|
2404
|
+
(q) => q.eq("collection", props.collection).eq("documentId", props.documentId).eq("status", "previewSnapshot")
|
|
2405
|
+
).first();
|
|
2406
|
+
if (!entry) return null;
|
|
2407
|
+
return entry.snapshot;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
// src/convex/vexQuery.ts
|
|
2411
|
+
import {
|
|
2412
|
+
queryGeneric
|
|
2413
|
+
} from "convex/server";
|
|
2414
|
+
import { v } from "convex/values";
|
|
2415
|
+
function vexQuery(props) {
|
|
2416
|
+
const mergedArgs = {
|
|
2417
|
+
...props.args,
|
|
2418
|
+
_vexDrafts: v.optional(v.union(v.literal("snapshot"), v.boolean()))
|
|
2419
|
+
};
|
|
2420
|
+
return queryGeneric({
|
|
2421
|
+
args: mergedArgs,
|
|
2422
|
+
handler: async (ctx, args) => {
|
|
2423
|
+
const { _vexDrafts, ...userArgs } = args;
|
|
2424
|
+
const drafts = _vexDrafts !== void 0 ? _vexDrafts : "snapshot";
|
|
2425
|
+
const vexCtx = Object.assign(Object.create(Object.getPrototypeOf(ctx)), ctx, {
|
|
2426
|
+
drafts
|
|
2427
|
+
});
|
|
2428
|
+
return props.handler(vexCtx, userArgs);
|
|
2429
|
+
}
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
// src/convex/model/collections.ts
|
|
2434
|
+
import { ConvexError } from "convex/values";
|
|
2435
|
+
async function resolveStorageUrl(ctx, doc) {
|
|
2436
|
+
if (doc?.storageId && (!doc.url || doc.url === "")) {
|
|
2437
|
+
const url = await ctx.storage.getUrl(doc.storageId);
|
|
2438
|
+
if (url) return { ...doc, url };
|
|
2439
|
+
}
|
|
2440
|
+
return doc;
|
|
2441
|
+
}
|
|
2442
|
+
async function listDocuments(props) {
|
|
2443
|
+
const { args, ctx } = props;
|
|
2444
|
+
const q = args.order === "desc" ? ctx.db.query(args.collectionSlug).order("desc") : ctx.db.query(args.collectionSlug);
|
|
2445
|
+
const result = await q.paginate(args.paginationOpts);
|
|
2446
|
+
const resolvedPage = await Promise.all(
|
|
2447
|
+
result.page.map((doc) => resolveStorageUrl(ctx, doc))
|
|
2448
|
+
);
|
|
2449
|
+
return { ...result, page: resolvedPage };
|
|
2450
|
+
}
|
|
2451
|
+
async function getDocument(props) {
|
|
2452
|
+
const doc = await props.ctx.db.get(props.args.documentId);
|
|
2453
|
+
if (!doc) return null;
|
|
2454
|
+
const resolved = await resolveStorageUrl(props.ctx, doc);
|
|
2455
|
+
if (props.args.preview) {
|
|
2456
|
+
const snapshot = await getPreviewSnapshot({
|
|
2457
|
+
ctx: props.ctx,
|
|
2458
|
+
collection: props.args.collectionSlug,
|
|
2459
|
+
documentId: props.args.documentId
|
|
2460
|
+
});
|
|
2461
|
+
if (snapshot) {
|
|
2462
|
+
return { ...resolved, ...snapshot };
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
return resolved;
|
|
2466
|
+
}
|
|
2467
|
+
async function updateDocument(props) {
|
|
2468
|
+
const f = { ...props.args.fields };
|
|
2469
|
+
if (f.storageId && f.url === "") {
|
|
2470
|
+
const url = await props.ctx.storage.getUrl(f.storageId);
|
|
2471
|
+
if (url) f.url = url;
|
|
2472
|
+
}
|
|
2473
|
+
const schema = generateFormSchema({
|
|
2474
|
+
fields: props.args.collectionFields
|
|
2475
|
+
}).partial();
|
|
2476
|
+
const result = schema.safeParse(f);
|
|
2477
|
+
if (!result.success) {
|
|
2478
|
+
throw new ConvexError({
|
|
2479
|
+
message: "Validation failed",
|
|
2480
|
+
errors: result.error.flatten()
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
await props.ctx.db.patch(props.args.documentId, result.data);
|
|
2484
|
+
return props.args.documentId;
|
|
2485
|
+
}
|
|
2486
|
+
async function createDocument(props) {
|
|
2487
|
+
if (props.args.kind === "global") {
|
|
2488
|
+
const existing = await props.ctx.db.query(props.args.collectionSlug).first();
|
|
2489
|
+
if (existing) {
|
|
2490
|
+
throw new ConvexError(
|
|
2491
|
+
`Global "${props.args.collectionSlug}" already exists. Globals can only have one document.`
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
const schema = generateFormSchema({
|
|
2496
|
+
fields: props.args.collectionFields
|
|
2497
|
+
});
|
|
2498
|
+
const result = schema.safeParse(props.args.fields);
|
|
2499
|
+
if (!result.success) {
|
|
2500
|
+
throw new ConvexError({
|
|
2501
|
+
message: "Validation failed",
|
|
2502
|
+
errors: result.error.flatten()
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
const id = await props.ctx.db.insert(props.args.collectionSlug, result.data);
|
|
2506
|
+
return id;
|
|
2507
|
+
}
|
|
2508
|
+
async function deleteDocument(props) {
|
|
2509
|
+
if (props.args.kind === "global") {
|
|
2510
|
+
const existing = await props.ctx.db.get(props.args.documentId);
|
|
2511
|
+
if (!existing) {
|
|
2512
|
+
throw new ConvexError(
|
|
2513
|
+
`Global "${props.args.collectionSlug}" document not found. Cannot delete a non-existent global.`
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
await props.ctx.db.delete(props.args.documentId);
|
|
2518
|
+
}
|
|
2519
|
+
async function searchDocuments(props) {
|
|
2520
|
+
const { args, ctx } = props;
|
|
2521
|
+
const docs = await ctx.db.query(args.collectionSlug).withSearchIndex(args.searchIndexName, (q) => q.search(args.searchField, args.query)).take(50);
|
|
2522
|
+
return Promise.all(docs.map((doc) => resolveStorageUrl(ctx, doc)));
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// src/valueTypes/generateCollectionQueries.ts
|
|
2526
|
+
var GENERATED_HEADER = "// \u26A0\uFE0F AUTO-GENERATED BY VEX CMS \u2014 DO NOT EDIT \u26A0\uFE0F";
|
|
2527
|
+
function generateCollectionQueries(props) {
|
|
2528
|
+
const { config, imports } = props;
|
|
2529
|
+
const result = {};
|
|
2530
|
+
const slugs = [];
|
|
2531
|
+
for (const collection of config.collections) {
|
|
2532
|
+
const { apiFile, modelFile } = generateCollectionPair({
|
|
2533
|
+
collection,
|
|
2534
|
+
isMedia: false,
|
|
2535
|
+
imports
|
|
2536
|
+
});
|
|
2537
|
+
result[`api/${collection.slug}.ts`] = apiFile;
|
|
2538
|
+
result[`model/api/${collection.slug}.ts`] = modelFile;
|
|
2539
|
+
slugs.push(collection.slug);
|
|
2540
|
+
}
|
|
2541
|
+
if (config.media?.collections) {
|
|
2542
|
+
for (const collection of config.media.collections) {
|
|
2543
|
+
const { apiFile, modelFile } = generateCollectionPair({
|
|
2544
|
+
collection,
|
|
2545
|
+
isMedia: true,
|
|
2546
|
+
imports
|
|
2547
|
+
});
|
|
2548
|
+
result[`api/${collection.slug}.ts`] = apiFile;
|
|
2549
|
+
result[`model/api/${collection.slug}.ts`] = modelFile;
|
|
2550
|
+
slugs.push(collection.slug);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
if (config.auth?.collections) {
|
|
2554
|
+
for (const collection of config.auth.collections) {
|
|
2555
|
+
if (!collection.generateApi) continue;
|
|
2556
|
+
if (slugs.includes(collection.slug)) continue;
|
|
2557
|
+
const { apiFile, modelFile } = generateCollectionPair({
|
|
2558
|
+
collection,
|
|
2559
|
+
isMedia: false,
|
|
2560
|
+
imports
|
|
2561
|
+
});
|
|
2562
|
+
result[`api/${collection.slug}.ts`] = apiFile;
|
|
2563
|
+
result[`model/api/${collection.slug}.ts`] = modelFile;
|
|
2564
|
+
slugs.push(collection.slug);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
result["api/index.ts"] = generateIndexFile({ slugs });
|
|
2568
|
+
return result;
|
|
2569
|
+
}
|
|
2570
|
+
function generateCollectionPair(props) {
|
|
2571
|
+
const { collection, isMedia, imports } = props;
|
|
2572
|
+
const slug = collection.slug;
|
|
2573
|
+
const tableName = collection.tableName ?? collection.slug;
|
|
2574
|
+
const firstSearchIndex = collection.searchIndexes?.[0] ?? null;
|
|
2575
|
+
const modelFile = generateModelFile({ slug, tableName, isMedia, firstSearchIndex, imports });
|
|
2576
|
+
const apiFile = generateApiFile({ slug, tableName, isMedia, firstSearchIndex, imports });
|
|
2577
|
+
return { apiFile, modelFile };
|
|
2578
|
+
}
|
|
2579
|
+
function generateModelFile(props) {
|
|
2580
|
+
const { tableName, isMedia, firstSearchIndex, imports } = props;
|
|
2581
|
+
const parts = [];
|
|
2582
|
+
const coreImports = ["getPreviewSnapshot"];
|
|
2583
|
+
if (!isMedia) {
|
|
2584
|
+
coreImports.push("generateFormSchema");
|
|
2585
|
+
}
|
|
2586
|
+
const coreTypeImports = ["CollectionKind"];
|
|
2587
|
+
if (!isMedia) {
|
|
2588
|
+
coreTypeImports.push("VexField");
|
|
2589
|
+
}
|
|
2590
|
+
const coreTypeImportLine = coreTypeImports.length > 0 ? `
|
|
2591
|
+
import type { ${coreTypeImports.join(", ")} } from "@vexcms/core"` : "";
|
|
2592
|
+
const convexTypeImports = isMedia ? "" : `
|
|
2593
|
+
import type { WithoutSystemFields } from "convex/server"`;
|
|
2594
|
+
parts.push(`${GENERATED_HEADER}
|
|
2595
|
+
import type { Doc, Id } from "${imports.generatedDirFromModel}/dataModel"
|
|
2596
|
+
import type { QueryCtx, MutationCtx } from "${imports.generatedDirFromModel}/server"${convexTypeImports}
|
|
2597
|
+
import { ConvexError } from "convex/values"
|
|
2598
|
+
import { ${coreImports.join(", ")} } from "@vexcms/core"${coreTypeImportLine}`);
|
|
2599
|
+
parts.push(buildModelGetDocument({ tableName }));
|
|
2600
|
+
parts.push(buildModelListDocuments({ tableName }));
|
|
2601
|
+
if (!isMedia) {
|
|
2602
|
+
parts.push(buildModelCreateDocument({ tableName }));
|
|
2603
|
+
}
|
|
2604
|
+
if (!isMedia) {
|
|
2605
|
+
parts.push(buildModelUpdateDocument({ tableName }));
|
|
2606
|
+
}
|
|
2607
|
+
parts.push(buildModelDeleteDocument({ tableName }));
|
|
2608
|
+
if (firstSearchIndex) {
|
|
2609
|
+
parts.push(buildModelSearchDocuments({
|
|
2610
|
+
tableName,
|
|
2611
|
+
searchIndexName: firstSearchIndex.name,
|
|
2612
|
+
searchField: firstSearchIndex.searchField
|
|
2613
|
+
}));
|
|
2614
|
+
}
|
|
2615
|
+
return parts.join("\n\n") + "\n";
|
|
2616
|
+
}
|
|
2617
|
+
function buildModelGetDocument(props) {
|
|
2618
|
+
const { tableName } = props;
|
|
2619
|
+
return `export async function getDocument(props: {
|
|
2620
|
+
ctx: QueryCtx
|
|
2621
|
+
documentId: Id<"${tableName}">
|
|
2622
|
+
preview?: boolean
|
|
2623
|
+
}): Promise<Doc<"${tableName}"> | null> {
|
|
2624
|
+
const doc = await props.ctx.db.get(props.documentId)
|
|
2625
|
+
if (!doc) return null
|
|
2626
|
+
|
|
2627
|
+
if (props.preview) {
|
|
2628
|
+
const snapshot = await getPreviewSnapshot({
|
|
2629
|
+
ctx: props.ctx,
|
|
2630
|
+
collection: "${tableName}",
|
|
2631
|
+
documentId: props.documentId,
|
|
2632
|
+
})
|
|
2633
|
+
if (snapshot) {
|
|
2634
|
+
return { ...doc, ...snapshot } as Doc<"${tableName}">
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
return doc
|
|
2639
|
+
}`;
|
|
2640
|
+
}
|
|
2641
|
+
function buildModelListDocuments(props) {
|
|
2642
|
+
const { tableName } = props;
|
|
2643
|
+
return `export async function listDocuments(props: {
|
|
2644
|
+
ctx: QueryCtx
|
|
2645
|
+
paginationOpts: { numItems: number; cursor: string | null }
|
|
2646
|
+
order?: "asc" | "desc"
|
|
2647
|
+
}) {
|
|
2648
|
+
const q = props.order === "desc"
|
|
2649
|
+
? props.ctx.db.query("${tableName}").order("desc")
|
|
2650
|
+
: props.ctx.db.query("${tableName}")
|
|
2651
|
+
return await q.paginate(props.paginationOpts)
|
|
2652
|
+
}`;
|
|
2653
|
+
}
|
|
2654
|
+
function buildModelCreateDocument(props) {
|
|
2655
|
+
const { tableName } = props;
|
|
2656
|
+
return `export async function createDocument(props: {
|
|
2657
|
+
collectionFields: Record<string, VexField>
|
|
2658
|
+
ctx: MutationCtx
|
|
2659
|
+
fields: unknown
|
|
2660
|
+
kind: CollectionKind
|
|
2661
|
+
}): Promise<Id<"${tableName}">> {
|
|
2662
|
+
if (props.kind === "global") {
|
|
2663
|
+
const existing = await props.ctx.db.query("${tableName}").first()
|
|
2664
|
+
if (existing) {
|
|
2665
|
+
throw new ConvexError(\`Global "${tableName}" already exists. Globals can only have one document.\`)
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
const schema = generateFormSchema({ fields: props.collectionFields })
|
|
2670
|
+
const parsed = schema.safeParse(props.fields)
|
|
2671
|
+
if (!parsed.success) {
|
|
2672
|
+
throw new ConvexError({ message: "Validation failed", errors: parsed.error.flatten() })
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
const data = { ...parsed.data }
|
|
2676
|
+
data.vex_status ??= "published"
|
|
2677
|
+
return await props.ctx.db.insert("${tableName}", data as WithoutSystemFields<Doc<"${tableName}">>)
|
|
2678
|
+
}`;
|
|
2679
|
+
}
|
|
2680
|
+
function buildModelUpdateDocument(props) {
|
|
2681
|
+
const { tableName } = props;
|
|
2682
|
+
return `export async function updateDocument(props: {
|
|
2683
|
+
collectionFields: Record<string, VexField>
|
|
2684
|
+
ctx: MutationCtx
|
|
2685
|
+
documentId: Id<"${tableName}">
|
|
2686
|
+
fields: unknown
|
|
2687
|
+
}): Promise<Id<"${tableName}">> {
|
|
2688
|
+
const schema = generateFormSchema({ fields: props.collectionFields }).partial()
|
|
2689
|
+
const parsed = schema.safeParse(props.fields)
|
|
2690
|
+
if (!parsed.success) {
|
|
2691
|
+
throw new ConvexError({ message: "Validation failed", errors: parsed.error.flatten() })
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
await props.ctx.db.patch(props.documentId, parsed.data as Partial<Doc<"${tableName}">>)
|
|
2695
|
+
return props.documentId
|
|
2696
|
+
}`;
|
|
2697
|
+
}
|
|
2698
|
+
function buildModelDeleteDocument(props) {
|
|
2699
|
+
const { tableName } = props;
|
|
2700
|
+
return `export async function deleteDocument(props: {
|
|
2701
|
+
ctx: MutationCtx
|
|
2702
|
+
documentId: Id<"${tableName}">
|
|
2703
|
+
kind: CollectionKind
|
|
2704
|
+
}): Promise<void> {
|
|
2705
|
+
if (props.kind === "global") {
|
|
2706
|
+
const existing = await props.ctx.db.get(props.documentId)
|
|
2707
|
+
if (!existing) {
|
|
2708
|
+
throw new ConvexError(\`Global "${tableName}" document not found. Cannot delete a non-existent global.\`)
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
await props.ctx.db.delete(props.documentId)
|
|
2713
|
+
}`;
|
|
2714
|
+
}
|
|
2715
|
+
function buildModelSearchDocuments(props) {
|
|
2716
|
+
const { tableName, searchIndexName, searchField } = props;
|
|
2717
|
+
return `export async function searchDocuments(props: {
|
|
2718
|
+
ctx: QueryCtx
|
|
2719
|
+
query: string
|
|
2720
|
+
}): Promise<Doc<"${tableName}">[]> {
|
|
2721
|
+
return await props.ctx.db.query("${tableName}")
|
|
2722
|
+
.withSearchIndex("${searchIndexName}", (q) => q.search("${searchField}", props.query))
|
|
2723
|
+
.take(50)
|
|
2724
|
+
}`;
|
|
2725
|
+
}
|
|
2726
|
+
function generateApiFile(props) {
|
|
2727
|
+
const { slug, tableName, isMedia, firstSearchIndex, imports } = props;
|
|
2728
|
+
const parts = [];
|
|
2729
|
+
const modelFns = [
|
|
2730
|
+
"getDocument",
|
|
2731
|
+
"listDocuments",
|
|
2732
|
+
...isMedia ? [] : ["createDocument", "updateDocument"],
|
|
2733
|
+
"deleteDocument",
|
|
2734
|
+
...firstSearchIndex ? ["searchDocuments"] : []
|
|
2735
|
+
];
|
|
2736
|
+
parts.push(`${GENERATED_HEADER}
|
|
2737
|
+
import { v } from "convex/values"
|
|
2738
|
+
import { paginationOptsValidator } from "convex/server"
|
|
2739
|
+
import { ConvexError } from "convex/values"
|
|
2740
|
+
import { query, mutation } from "${imports.generatedDirFromApi}/server"
|
|
2741
|
+
import type { QueryCtx, MutationCtx } from "${imports.generatedDirFromApi}/server"
|
|
2742
|
+
import { hasPermission, findCollectionBySlug } from "@vexcms/core"
|
|
2743
|
+
import { getUser } from "${imports.authFromApi}"
|
|
2744
|
+
import vexConfig from "${imports.vexConfigFromApi}"
|
|
2745
|
+
import { ${modelFns.join(", ")} } from "../model/api/${slug}"`);
|
|
2746
|
+
parts.push(`const SLUG = "${slug}" as const
|
|
2747
|
+
|
|
2748
|
+
async function requireAuth(ctx: QueryCtx | MutationCtx) {
|
|
2749
|
+
const auth = await getUser(ctx)
|
|
2750
|
+
if (!auth) throw new ConvexError("Not authenticated")
|
|
2751
|
+
return auth
|
|
2752
|
+
}`);
|
|
2753
|
+
parts.push(buildApiGetDocument({ tableName }));
|
|
2754
|
+
parts.push(buildApiListDocuments({ tableName }));
|
|
2755
|
+
if (!isMedia) {
|
|
2756
|
+
parts.push(buildApiCreateDocument({ slug, tableName }));
|
|
2757
|
+
}
|
|
2758
|
+
if (!isMedia) {
|
|
2759
|
+
parts.push(buildApiUpdateDocument({ slug, tableName }));
|
|
2760
|
+
}
|
|
2761
|
+
parts.push(buildApiDeleteDocument({ tableName }));
|
|
2762
|
+
if (firstSearchIndex) {
|
|
2763
|
+
parts.push(buildApiSearchDocuments());
|
|
2764
|
+
}
|
|
2765
|
+
return parts.join("\n\n") + "\n";
|
|
2766
|
+
}
|
|
2767
|
+
function buildApiGetDocument(props) {
|
|
2768
|
+
const { tableName } = props;
|
|
2769
|
+
return `export const get = query({
|
|
2770
|
+
args: {
|
|
2771
|
+
id: v.id("${tableName}"),
|
|
2772
|
+
_vexDrafts: v.optional(v.union(v.literal("snapshot"), v.boolean())),
|
|
2773
|
+
},
|
|
2774
|
+
handler: async (ctx, args) => {
|
|
2775
|
+
const preview = (args._vexDrafts ?? "snapshot") === "snapshot"
|
|
2776
|
+
const doc = await getDocument({
|
|
2777
|
+
ctx,
|
|
2778
|
+
documentId: args.id,
|
|
2779
|
+
preview,
|
|
2780
|
+
})
|
|
2781
|
+
if (!doc) return null
|
|
2782
|
+
const auth = await getUser(ctx)
|
|
2783
|
+
if (auth) {
|
|
2784
|
+
const allowed = hasPermission({
|
|
2785
|
+
access: vexConfig.access,
|
|
2786
|
+
user: auth.user,
|
|
2787
|
+
userRoles: auth.roles,
|
|
2788
|
+
resource: SLUG,
|
|
2789
|
+
action: "read",
|
|
2790
|
+
data: doc,
|
|
2791
|
+
})
|
|
2792
|
+
if (!allowed) return null
|
|
2793
|
+
}
|
|
2794
|
+
return doc
|
|
2795
|
+
},
|
|
2796
|
+
})`;
|
|
2797
|
+
}
|
|
2798
|
+
function buildApiListDocuments(_props) {
|
|
2799
|
+
return `export const list = query({
|
|
2800
|
+
args: {
|
|
2801
|
+
paginationOpts: paginationOptsValidator,
|
|
2802
|
+
order: v.optional(v.union(v.literal("asc"), v.literal("desc"))),
|
|
2803
|
+
},
|
|
2804
|
+
handler: async (ctx, args) => {
|
|
2805
|
+
const result = await listDocuments({
|
|
2806
|
+
ctx,
|
|
2807
|
+
paginationOpts: args.paginationOpts,
|
|
2808
|
+
order: args.order,
|
|
2809
|
+
})
|
|
2810
|
+
const auth = await getUser(ctx)
|
|
2811
|
+
if (!auth) return result
|
|
2812
|
+
const filteredPage = result.page.filter((doc) =>
|
|
2813
|
+
hasPermission({
|
|
2814
|
+
access: vexConfig.access,
|
|
2815
|
+
user: auth.user,
|
|
2816
|
+
userRoles: auth.roles,
|
|
2817
|
+
resource: SLUG,
|
|
2818
|
+
action: "read",
|
|
2819
|
+
data: doc,
|
|
2820
|
+
}) === true,
|
|
2821
|
+
)
|
|
2822
|
+
return { ...result, page: filteredPage }
|
|
2823
|
+
},
|
|
2824
|
+
})`;
|
|
2825
|
+
}
|
|
2826
|
+
function buildApiCreateDocument(_props) {
|
|
2827
|
+
return `export const create = mutation({
|
|
2828
|
+
args: { fields: v.any() },
|
|
2829
|
+
handler: async (ctx, args) => {
|
|
2830
|
+
const { user, roles } = await requireAuth(ctx)
|
|
2831
|
+
hasPermission({
|
|
2832
|
+
access: vexConfig.access,
|
|
2833
|
+
user,
|
|
2834
|
+
userRoles: roles,
|
|
2835
|
+
resource: SLUG,
|
|
2836
|
+
action: "create",
|
|
2837
|
+
throwOnDenied: true,
|
|
2838
|
+
})
|
|
2839
|
+
const collection = findCollectionBySlug({ slug: SLUG, config: vexConfig })
|
|
2840
|
+
if (!collection) throw new ConvexError(\`Collection "\${SLUG}" not found in vex config\`)
|
|
2841
|
+
return createDocument({
|
|
2842
|
+
collectionFields: collection.fields,
|
|
2843
|
+
ctx,
|
|
2844
|
+
fields: args.fields as unknown,
|
|
2845
|
+
kind: "collection",
|
|
2846
|
+
})
|
|
2847
|
+
},
|
|
2848
|
+
})`;
|
|
2849
|
+
}
|
|
2850
|
+
function buildApiUpdateDocument(props) {
|
|
2851
|
+
const { tableName } = props;
|
|
2852
|
+
return `export const update = mutation({
|
|
2853
|
+
args: { id: v.id("${tableName}"), fields: v.any() },
|
|
2854
|
+
handler: async (ctx, args) => {
|
|
2855
|
+
const { user, roles } = await requireAuth(ctx)
|
|
2856
|
+
hasPermission({
|
|
2857
|
+
access: vexConfig.access,
|
|
2858
|
+
user,
|
|
2859
|
+
userRoles: roles,
|
|
2860
|
+
resource: SLUG,
|
|
2861
|
+
action: "update",
|
|
2862
|
+
throwOnDenied: true,
|
|
2863
|
+
})
|
|
2864
|
+
const collection = findCollectionBySlug({ slug: SLUG, config: vexConfig })
|
|
2865
|
+
if (!collection) throw new ConvexError(\`Collection "\${SLUG}" not found in vex config\`)
|
|
2866
|
+
return updateDocument({
|
|
2867
|
+
collectionFields: collection.fields,
|
|
2868
|
+
ctx,
|
|
2869
|
+
documentId: args.id,
|
|
2870
|
+
fields: args.fields,
|
|
2871
|
+
})
|
|
2872
|
+
},
|
|
2873
|
+
})`;
|
|
2874
|
+
}
|
|
2875
|
+
function buildApiDeleteDocument(props) {
|
|
2876
|
+
const { tableName } = props;
|
|
2877
|
+
return `export const remove = mutation({
|
|
2878
|
+
args: { id: v.id("${tableName}") },
|
|
2879
|
+
handler: async (ctx, args) => {
|
|
2880
|
+
const { user, roles } = await requireAuth(ctx)
|
|
2881
|
+
hasPermission({
|
|
2882
|
+
access: vexConfig.access,
|
|
2883
|
+
user,
|
|
2884
|
+
userRoles: roles,
|
|
2885
|
+
resource: SLUG,
|
|
2886
|
+
action: "delete",
|
|
2887
|
+
throwOnDenied: true,
|
|
2888
|
+
})
|
|
2889
|
+
await deleteDocument({
|
|
2890
|
+
ctx,
|
|
2891
|
+
documentId: args.id,
|
|
2892
|
+
kind: "collection",
|
|
2893
|
+
})
|
|
2894
|
+
},
|
|
2895
|
+
})`;
|
|
2896
|
+
}
|
|
2897
|
+
function buildApiSearchDocuments() {
|
|
2898
|
+
return `export const search = query({
|
|
2899
|
+
args: { query: v.string() },
|
|
2900
|
+
handler: async (ctx, args) => {
|
|
2901
|
+
const results = await searchDocuments({
|
|
2902
|
+
ctx,
|
|
2903
|
+
query: args.query,
|
|
2904
|
+
})
|
|
2905
|
+
const auth = await getUser(ctx)
|
|
2906
|
+
if (!auth) return results
|
|
2907
|
+
return results.filter((doc) =>
|
|
2908
|
+
hasPermission({
|
|
2909
|
+
access: vexConfig.access,
|
|
2910
|
+
user: auth.user,
|
|
2911
|
+
userRoles: auth.roles,
|
|
2912
|
+
resource: SLUG,
|
|
2913
|
+
action: "read",
|
|
2914
|
+
data: doc,
|
|
2915
|
+
}) === true,
|
|
2916
|
+
)
|
|
2917
|
+
},
|
|
2918
|
+
})`;
|
|
2919
|
+
}
|
|
2920
|
+
function generateIndexFile(props) {
|
|
2921
|
+
const sorted = [...props.slugs].sort();
|
|
2922
|
+
if (sorted.length === 0) {
|
|
2923
|
+
return GENERATED_HEADER + "\n";
|
|
2924
|
+
}
|
|
2925
|
+
const exports = sorted.map((slug) => `export * as ${slug} from "./${slug}"`).join("\n");
|
|
2926
|
+
return `${GENERATED_HEADER}
|
|
2927
|
+
${exports}
|
|
2928
|
+
`;
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
// src/migrations/diffSchema.ts
|
|
2932
|
+
function parseTables(schema) {
|
|
2933
|
+
const tables = /* @__PURE__ */ new Map();
|
|
2934
|
+
if (!schema.trim()) return tables;
|
|
2935
|
+
const tableRegex = /export\s+const\s+(\w+)\s*=\s*defineTable\(\{([\s\S]*?)\}\)/g;
|
|
2936
|
+
let match;
|
|
2937
|
+
while ((match = tableRegex.exec(schema)) !== null) {
|
|
2938
|
+
const name = match[1];
|
|
2939
|
+
const body = match[2];
|
|
2940
|
+
const fields = /* @__PURE__ */ new Map();
|
|
2941
|
+
const fieldRegex = /^\s+(\w+):\s+(v\..+?),?\s*$/gm;
|
|
2942
|
+
let fieldMatch;
|
|
2943
|
+
while ((fieldMatch = fieldRegex.exec(body)) !== null) {
|
|
2944
|
+
const fieldName = fieldMatch[1];
|
|
2945
|
+
const valueType = fieldMatch[2].replace(/,\s*$/, "");
|
|
2946
|
+
const isOptional = valueType.startsWith("v.optional(");
|
|
2947
|
+
fields.set(fieldName, { valueType, isOptional });
|
|
2948
|
+
}
|
|
2949
|
+
tables.set(name, { name, fields });
|
|
2950
|
+
}
|
|
2951
|
+
return tables;
|
|
2952
|
+
}
|
|
2953
|
+
function diffSchema(oldSchema, newSchema) {
|
|
2954
|
+
const oldTables = parseTables(oldSchema);
|
|
2955
|
+
const newTables = parseTables(newSchema);
|
|
2956
|
+
const addedRequired = [];
|
|
2957
|
+
const addedOptional = [];
|
|
2958
|
+
const newRequired = [];
|
|
2959
|
+
const removedFields = [];
|
|
2960
|
+
for (const [tableName, newTable] of newTables) {
|
|
2961
|
+
const oldTable = oldTables.get(tableName);
|
|
2962
|
+
for (const [fieldName, newField] of newTable.fields) {
|
|
2963
|
+
const info = {
|
|
2964
|
+
table: tableName,
|
|
2965
|
+
field: fieldName,
|
|
2966
|
+
valueType: newField.valueType,
|
|
2967
|
+
isOptional: newField.isOptional
|
|
2968
|
+
};
|
|
2969
|
+
if (!oldTable) {
|
|
2970
|
+
if (newField.isOptional) {
|
|
2971
|
+
addedOptional.push(info);
|
|
2972
|
+
} else {
|
|
2973
|
+
addedRequired.push(info);
|
|
2974
|
+
}
|
|
2975
|
+
} else {
|
|
2976
|
+
const oldField = oldTable.fields.get(fieldName);
|
|
2977
|
+
if (!oldField) {
|
|
2978
|
+
if (newField.isOptional) {
|
|
2979
|
+
addedOptional.push(info);
|
|
2980
|
+
} else {
|
|
2981
|
+
addedRequired.push(info);
|
|
2982
|
+
}
|
|
2983
|
+
} else if (oldField.isOptional && !newField.isOptional) {
|
|
2984
|
+
newRequired.push(info);
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
for (const [tableName, oldTable] of oldTables) {
|
|
2990
|
+
const newTable = newTables.get(tableName);
|
|
2991
|
+
if (!newTable) continue;
|
|
2992
|
+
for (const [fieldName, oldField] of oldTable.fields) {
|
|
2993
|
+
if (!newTable.fields.has(fieldName)) {
|
|
2994
|
+
removedFields.push({
|
|
2995
|
+
table: tableName,
|
|
2996
|
+
field: fieldName,
|
|
2997
|
+
valueType: oldField.valueType,
|
|
2998
|
+
wasOptional: oldField.isOptional
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
return {
|
|
3004
|
+
addedRequired,
|
|
3005
|
+
addedOptional,
|
|
3006
|
+
newRequired,
|
|
3007
|
+
removedFields,
|
|
3008
|
+
needsMigration: [...addedRequired, ...addedOptional, ...newRequired]
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
function makeFieldsOptional(schema, fields) {
|
|
3012
|
+
let result = schema;
|
|
3013
|
+
for (const field of fields) {
|
|
3014
|
+
if (field.isOptional) continue;
|
|
3015
|
+
const escaped = field.field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3016
|
+
const pattern = new RegExp(
|
|
3017
|
+
`(\\s+${escaped}:\\s+)(${field.valueType.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})(,?)`
|
|
3018
|
+
);
|
|
3019
|
+
result = result.replace(pattern, `$1v.optional($2)$3`);
|
|
3020
|
+
}
|
|
3021
|
+
return result;
|
|
3022
|
+
}
|
|
3023
|
+
function addRemovedFieldsAsOptional(schema, fields) {
|
|
3024
|
+
let result = schema;
|
|
3025
|
+
const byTable = /* @__PURE__ */ new Map();
|
|
3026
|
+
for (const f of fields) {
|
|
3027
|
+
const list = byTable.get(f.table) ?? [];
|
|
3028
|
+
list.push(f);
|
|
3029
|
+
byTable.set(f.table, list);
|
|
3030
|
+
}
|
|
3031
|
+
for (const [tableName, tableFields] of byTable) {
|
|
3032
|
+
const tablePattern = new RegExp(
|
|
3033
|
+
`(export\\s+const\\s+${tableName}\\s*=\\s*defineTable\\(\\{[\\s\\S]*?)(\\}\\))`
|
|
3034
|
+
);
|
|
3035
|
+
const tableMatch = result.match(tablePattern);
|
|
3036
|
+
if (!tableMatch) continue;
|
|
3037
|
+
const extraLines = tableFields.map((f) => {
|
|
3038
|
+
const optionalType = f.wasOptional ? f.valueType : `v.optional(${f.valueType})`;
|
|
3039
|
+
return ` ${f.field}: ${optionalType},`;
|
|
3040
|
+
});
|
|
3041
|
+
result = result.replace(
|
|
3042
|
+
tablePattern,
|
|
3043
|
+
`$1${extraLines.join("\n")}
|
|
3044
|
+
$2`
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
3047
|
+
return result;
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
// src/migrations/planMigration.ts
|
|
3051
|
+
function planMigration(props) {
|
|
3052
|
+
const { diff, config } = props;
|
|
3053
|
+
if (diff.needsMigration.length === 0) return [];
|
|
3054
|
+
const tableFieldsMap = /* @__PURE__ */ new Map();
|
|
3055
|
+
for (const collection of config.collections) {
|
|
3056
|
+
tableFieldsMap.set(collection.slug, collection.fields);
|
|
3057
|
+
}
|
|
3058
|
+
for (const global of config.globals) {
|
|
3059
|
+
tableFieldsMap.set(global.slug, global.fields);
|
|
3060
|
+
}
|
|
3061
|
+
const ops = [];
|
|
3062
|
+
for (const fieldInfo of diff.needsMigration) {
|
|
3063
|
+
const collectionFields = tableFieldsMap.get(fieldInfo.table);
|
|
3064
|
+
if (!collectionFields) {
|
|
3065
|
+
continue;
|
|
3066
|
+
}
|
|
3067
|
+
const field = collectionFields[fieldInfo.field];
|
|
3068
|
+
if (!field) {
|
|
3069
|
+
continue;
|
|
3070
|
+
}
|
|
3071
|
+
if (!field.required) {
|
|
3072
|
+
continue;
|
|
3073
|
+
}
|
|
3074
|
+
const defaultValue = field.defaultValue;
|
|
3075
|
+
if (defaultValue === void 0) {
|
|
3076
|
+
continue;
|
|
3077
|
+
}
|
|
3078
|
+
ops.push({
|
|
3079
|
+
table: fieldInfo.table,
|
|
3080
|
+
field: fieldInfo.field,
|
|
3081
|
+
defaultValue
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
return ops;
|
|
3085
|
+
}
|
|
3086
|
+
export {
|
|
3087
|
+
ALL_SYSTEM_FIELDS,
|
|
3088
|
+
DEFAULT_AUTOSAVE_INTERVAL,
|
|
3089
|
+
DEFAULT_BREAKPOINTS,
|
|
3090
|
+
DEFAULT_MAX_VERSIONS_PER_DOC,
|
|
3091
|
+
GENERATED_HEADER,
|
|
3092
|
+
LOCKED_MEDIA_FIELDS,
|
|
3093
|
+
OVERRIDABLE_MEDIA_FIELDS,
|
|
3094
|
+
PREVIEW_SNAPSHOT_DEBOUNCE_MS,
|
|
3095
|
+
VERSION_SYSTEM_FIELDS,
|
|
3096
|
+
VexAccessConfigError,
|
|
3097
|
+
VexAccessError,
|
|
3098
|
+
VexAuthConfigError,
|
|
3099
|
+
VexBlockValidationError,
|
|
3100
|
+
VexError,
|
|
3101
|
+
VexFieldValidationError,
|
|
3102
|
+
VexMediaConfigError,
|
|
3103
|
+
VexSlugConflictError,
|
|
3104
|
+
addRemovedFieldsAsOptional,
|
|
3105
|
+
array,
|
|
3106
|
+
blocks,
|
|
3107
|
+
checkbox,
|
|
3108
|
+
createDocument,
|
|
3109
|
+
date,
|
|
3110
|
+
defineAccess,
|
|
3111
|
+
defineBlock,
|
|
3112
|
+
defineCollection,
|
|
3113
|
+
defineConfig,
|
|
3114
|
+
defineMediaCollection,
|
|
3115
|
+
deleteDocument,
|
|
3116
|
+
deletePreviewSnapshot,
|
|
3117
|
+
diffSchema,
|
|
3118
|
+
extendTable,
|
|
3119
|
+
extractLivePreviewConfigs,
|
|
3120
|
+
extractUserFields,
|
|
3121
|
+
fieldMetaToZod,
|
|
3122
|
+
findCollectionBySlug,
|
|
3123
|
+
generateCollectionQueries,
|
|
3124
|
+
generateColumns,
|
|
3125
|
+
generateFormDefaultValues,
|
|
3126
|
+
generateFormSchema,
|
|
3127
|
+
generateIndexFile,
|
|
3128
|
+
generateVexSchema,
|
|
3129
|
+
generateVexTypes,
|
|
3130
|
+
getAllCollections,
|
|
3131
|
+
getDocument,
|
|
3132
|
+
getPreviewSnapshot,
|
|
3133
|
+
hasPermission,
|
|
3134
|
+
imageUrl,
|
|
3135
|
+
isMediaCollection,
|
|
3136
|
+
json,
|
|
3137
|
+
listDocuments,
|
|
3138
|
+
makeFieldsOptional,
|
|
3139
|
+
mergeAuthCollectionWithUserCollection,
|
|
3140
|
+
number,
|
|
3141
|
+
planMigration,
|
|
3142
|
+
relationship,
|
|
3143
|
+
resolvePreviewURL,
|
|
3144
|
+
richtext,
|
|
3145
|
+
sanitizeConfigForClient,
|
|
3146
|
+
searchDocuments,
|
|
3147
|
+
select,
|
|
3148
|
+
shouldReloadURL,
|
|
3149
|
+
slugToInterfaceName,
|
|
3150
|
+
text,
|
|
3151
|
+
toTitleCase,
|
|
3152
|
+
ui,
|
|
3153
|
+
updateDocument,
|
|
3154
|
+
upload,
|
|
3155
|
+
upsertPreviewSnapshot,
|
|
3156
|
+
vexQuery
|
|
3157
|
+
};
|
|
3158
|
+
//# sourceMappingURL=index.js.map
|