@vexcms/core 0.0.19 → 0.1.0-alpha.2
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/LICENSE +202 -0
- package/dist/.build +1 -0
- package/dist/access/buildChecks.d.ts +58 -0
- package/dist/access/buildChecks.d.ts.map +1 -0
- package/dist/access/canAccessAdminPanel.d.ts +53 -0
- package/dist/access/canAccessAdminPanel.d.ts.map +1 -0
- package/dist/access/compileConstraints.d.ts +155 -0
- package/dist/access/compileConstraints.d.ts.map +1 -0
- package/dist/access/config.d.ts +62 -0
- package/dist/access/config.d.ts.map +1 -0
- package/dist/access/constants.d.ts +95 -0
- package/dist/access/constants.d.ts.map +1 -0
- package/dist/access/constraintTypes.d.ts +337 -0
- package/dist/access/constraintTypes.d.ts.map +1 -0
- package/dist/access/createAccessQueryBuilder.d.ts +59 -0
- package/dist/access/createAccessQueryBuilder.d.ts.map +1 -0
- package/dist/access/createFilterConstraintBuilder.d.ts +26 -0
- package/dist/access/createFilterConstraintBuilder.d.ts.map +1 -0
- package/dist/access/createIndexConstraintBuilder.d.ts +40 -0
- package/dist/access/createIndexConstraintBuilder.d.ts.map +1 -0
- package/dist/access/hasPermission.d.ts +85 -0
- package/dist/access/hasPermission.d.ts.map +1 -0
- package/dist/access/index.d.ts +13 -0
- package/dist/access/index.d.ts.map +1 -0
- package/dist/access/pickQueryIndex.d.ts +42 -0
- package/dist/access/pickQueryIndex.d.ts.map +1 -0
- package/dist/access/resolveAccessRule.d.ts +85 -0
- package/dist/access/resolveAccessRule.d.ts.map +1 -0
- package/dist/access/types.d.ts +738 -0
- package/dist/access/types.d.ts.map +1 -0
- package/dist/access/validateAccessConstraints.d.ts +57 -0
- package/dist/access/validateAccessConstraints.d.ts.map +1 -0
- package/dist/api/client.d.ts +20 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +117 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/convex.d.ts +296 -0
- package/dist/api/convex.d.ts.map +1 -0
- package/dist/api/create/client.d.ts +50 -0
- package/dist/api/create/client.d.ts.map +1 -0
- package/dist/api/create/server.d.ts +43 -0
- package/dist/api/create/server.d.ts.map +1 -0
- package/dist/api/depth.d.ts +39 -0
- package/dist/api/depth.d.ts.map +1 -0
- package/dist/api/find/client.d.ts +90 -0
- package/dist/api/find/client.d.ts.map +1 -0
- package/dist/api/find/server.d.ts +73 -0
- package/dist/api/find/server.d.ts.map +1 -0
- package/dist/api/get/client.d.ts +56 -0
- package/dist/api/get/client.d.ts.map +1 -0
- package/dist/api/get/server.d.ts +49 -0
- package/dist/api/get/server.d.ts.map +1 -0
- package/dist/api/globals/find.client.d.ts +20 -0
- package/dist/api/globals/find.client.d.ts.map +1 -0
- package/dist/api/globals/find.server.d.ts +29 -0
- package/dist/api/globals/find.server.d.ts.map +1 -0
- package/dist/api/globals/get.client.d.ts +46 -0
- package/dist/api/globals/get.client.d.ts.map +1 -0
- package/dist/api/globals/get.server.d.ts +55 -0
- package/dist/api/globals/get.server.d.ts.map +1 -0
- package/dist/api/globals/types.d.ts +65 -0
- package/dist/api/globals/types.d.ts.map +1 -0
- package/dist/api/globals/upsert.client.d.ts +24 -0
- package/dist/api/globals/upsert.client.d.ts.map +1 -0
- package/dist/api/globals/upsert.server.d.ts +47 -0
- package/dist/api/globals/upsert.server.d.ts.map +1 -0
- package/dist/api/populate.d.ts +23 -0
- package/dist/api/populate.d.ts.map +1 -0
- package/dist/api/remove/client.d.ts +58 -0
- package/dist/api/remove/client.d.ts.map +1 -0
- package/dist/api/remove/server.d.ts +65 -0
- package/dist/api/remove/server.d.ts.map +1 -0
- package/dist/api/search/client.d.ts +58 -0
- package/dist/api/search/client.d.ts.map +1 -0
- package/dist/api/search/server.d.ts +54 -0
- package/dist/api/search/server.d.ts.map +1 -0
- package/dist/api/server.d.ts +370 -0
- package/dist/api/server.d.ts.map +1 -0
- package/dist/api/server.js +33 -0
- package/dist/api/server.js.map +1 -0
- package/dist/api/types.d.ts +525 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/update/client.d.ts +53 -0
- package/dist/api/update/client.d.ts.map +1 -0
- package/dist/api/update/server.d.ts +47 -0
- package/dist/api/update/server.d.ts.map +1 -0
- package/dist/api/utils.d.ts +57 -0
- package/dist/api/utils.d.ts.map +1 -0
- package/dist/auth/mergeCollections.d.ts +28 -0
- package/dist/auth/mergeCollections.d.ts.map +1 -0
- package/dist/auth/types.d.ts +84 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/chunk-2DU7JDEX.js +1877 -0
- package/dist/chunk-2DU7JDEX.js.map +1 -0
- package/dist/chunk-2IB2DCN7.js +75 -0
- package/dist/chunk-2IB2DCN7.js.map +1 -0
- package/dist/collections/config.d.ts +28 -0
- package/dist/collections/config.d.ts.map +1 -0
- package/dist/collections/constants.d.ts +27 -0
- package/dist/collections/constants.d.ts.map +1 -0
- package/dist/collections/index.d.ts +7 -0
- package/dist/collections/index.d.ts.map +1 -0
- package/dist/collections/indexFields.d.ts +20 -0
- package/dist/collections/indexFields.d.ts.map +1 -0
- package/dist/collections/interfaceGen.d.ts +59 -0
- package/dist/collections/interfaceGen.d.ts.map +1 -0
- package/dist/collections/internal/index.d.ts +2 -0
- package/dist/collections/internal/index.d.ts.map +1 -0
- package/dist/collections/types.d.ts +282 -0
- package/dist/collections/types.d.ts.map +1 -0
- package/dist/collections/utils.d.ts +71 -0
- package/dist/collections/utils.d.ts.map +1 -0
- package/dist/collections/validator.d.ts +73 -0
- package/dist/collections/validator.d.ts.map +1 -0
- package/dist/config/config.d.ts +43 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/sanitizeConfig.d.ts +82 -0
- package/dist/config/sanitizeConfig.d.ts.map +1 -0
- package/dist/config/types.d.ts +316 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/fields/array/config.d.ts +57 -0
- package/dist/fields/array/config.d.ts.map +1 -0
- package/dist/fields/array/index.d.ts +5 -0
- package/dist/fields/array/index.d.ts.map +1 -0
- package/dist/fields/array/inputSchema.d.ts +32 -0
- package/dist/fields/array/inputSchema.d.ts.map +1 -0
- package/dist/fields/array/types.d.ts +127 -0
- package/dist/fields/array/types.d.ts.map +1 -0
- package/dist/fields/array/validator.d.ts +30 -0
- package/dist/fields/array/validator.d.ts.map +1 -0
- package/dist/fields/baseTypes.d.ts +251 -0
- package/dist/fields/baseTypes.d.ts.map +1 -0
- package/dist/fields/blocks/config.d.ts +63 -0
- package/dist/fields/blocks/config.d.ts.map +1 -0
- package/dist/fields/blocks/index.d.ts +5 -0
- package/dist/fields/blocks/index.d.ts.map +1 -0
- package/dist/fields/blocks/inputSchema.d.ts +22 -0
- package/dist/fields/blocks/inputSchema.d.ts.map +1 -0
- package/dist/fields/blocks/types.d.ts +253 -0
- package/dist/fields/blocks/types.d.ts.map +1 -0
- package/dist/fields/blocks/validator.d.ts +32 -0
- package/dist/fields/blocks/validator.d.ts.map +1 -0
- package/dist/fields/checkbox/config.d.ts +46 -0
- package/dist/fields/checkbox/config.d.ts.map +1 -0
- package/dist/fields/checkbox/index.d.ts +5 -0
- package/dist/fields/checkbox/index.d.ts.map +1 -0
- package/dist/fields/checkbox/inputSchema.d.ts +27 -0
- package/dist/fields/checkbox/inputSchema.d.ts.map +1 -0
- package/dist/fields/checkbox/types.d.ts +67 -0
- package/dist/fields/checkbox/types.d.ts.map +1 -0
- package/dist/fields/checkbox/validator.d.ts +34 -0
- package/dist/fields/checkbox/validator.d.ts.map +1 -0
- package/dist/fields/color/config.d.ts +49 -0
- package/dist/fields/color/config.d.ts.map +1 -0
- package/dist/fields/color/convert.d.ts +72 -0
- package/dist/fields/color/convert.d.ts.map +1 -0
- package/dist/fields/color/formats.d.ts +38 -0
- package/dist/fields/color/formats.d.ts.map +1 -0
- package/dist/fields/color/index.d.ts +8 -0
- package/dist/fields/color/index.d.ts.map +1 -0
- package/dist/fields/color/inputSchema.d.ts +29 -0
- package/dist/fields/color/inputSchema.d.ts.map +1 -0
- package/dist/fields/color/types.d.ts +100 -0
- package/dist/fields/color/types.d.ts.map +1 -0
- package/dist/fields/color/utils.d.ts +100 -0
- package/dist/fields/color/utils.d.ts.map +1 -0
- package/dist/fields/color/validator.d.ts +27 -0
- package/dist/fields/color/validator.d.ts.map +1 -0
- package/dist/fields/constants.d.ts +108 -0
- package/dist/fields/constants.d.ts.map +1 -0
- package/dist/fields/date/config.d.ts +57 -0
- package/dist/fields/date/config.d.ts.map +1 -0
- package/dist/fields/date/index.d.ts +5 -0
- package/dist/fields/date/index.d.ts.map +1 -0
- package/dist/fields/date/inputSchema.d.ts +29 -0
- package/dist/fields/date/inputSchema.d.ts.map +1 -0
- package/dist/fields/date/types.d.ts +147 -0
- package/dist/fields/date/types.d.ts.map +1 -0
- package/dist/fields/date/validator.d.ts +34 -0
- package/dist/fields/date/validator.d.ts.map +1 -0
- package/dist/fields/group/config.d.ts +47 -0
- package/dist/fields/group/config.d.ts.map +1 -0
- package/dist/fields/group/index.d.ts +5 -0
- package/dist/fields/group/index.d.ts.map +1 -0
- package/dist/fields/group/inputSchema.d.ts +27 -0
- package/dist/fields/group/inputSchema.d.ts.map +1 -0
- package/dist/fields/group/types.d.ts +105 -0
- package/dist/fields/group/types.d.ts.map +1 -0
- package/dist/fields/group/validator.d.ts +41 -0
- package/dist/fields/group/validator.d.ts.map +1 -0
- package/dist/fields/index.d.ts +20 -0
- package/dist/fields/index.d.ts.map +1 -0
- package/dist/fields/inputSchemas/index.d.ts +23 -0
- package/dist/fields/inputSchemas/index.d.ts.map +1 -0
- package/dist/fields/inputSchemas/utils.d.ts +25 -0
- package/dist/fields/inputSchemas/utils.d.ts.map +1 -0
- package/dist/fields/interfaceGen.d.ts +15 -0
- package/dist/fields/interfaceGen.d.ts.map +1 -0
- package/dist/fields/number/config.d.ts +48 -0
- package/dist/fields/number/config.d.ts.map +1 -0
- package/dist/fields/number/index.d.ts +5 -0
- package/dist/fields/number/index.d.ts.map +1 -0
- package/dist/fields/number/inputSchema.d.ts +23 -0
- package/dist/fields/number/inputSchema.d.ts.map +1 -0
- package/dist/fields/number/types.d.ts +100 -0
- package/dist/fields/number/types.d.ts.map +1 -0
- package/dist/fields/number/validator.d.ts +39 -0
- package/dist/fields/number/validator.d.ts.map +1 -0
- package/dist/fields/relationship/config.d.ts +45 -0
- package/dist/fields/relationship/config.d.ts.map +1 -0
- package/dist/fields/relationship/index.d.ts +5 -0
- package/dist/fields/relationship/index.d.ts.map +1 -0
- package/dist/fields/relationship/inputSchema.d.ts +31 -0
- package/dist/fields/relationship/inputSchema.d.ts.map +1 -0
- package/dist/fields/relationship/types.d.ts +126 -0
- package/dist/fields/relationship/types.d.ts.map +1 -0
- package/dist/fields/relationship/validator.d.ts +27 -0
- package/dist/fields/relationship/validator.d.ts.map +1 -0
- package/dist/fields/select/config.d.ts +59 -0
- package/dist/fields/select/config.d.ts.map +1 -0
- package/dist/fields/select/index.d.ts +5 -0
- package/dist/fields/select/index.d.ts.map +1 -0
- package/dist/fields/select/inputSchema.d.ts +28 -0
- package/dist/fields/select/inputSchema.d.ts.map +1 -0
- package/dist/fields/select/types.d.ts +89 -0
- package/dist/fields/select/types.d.ts.map +1 -0
- package/dist/fields/select/validator.d.ts +31 -0
- package/dist/fields/select/validator.d.ts.map +1 -0
- package/dist/fields/text/config.d.ts +48 -0
- package/dist/fields/text/config.d.ts.map +1 -0
- package/dist/fields/text/index.d.ts +5 -0
- package/dist/fields/text/index.d.ts.map +1 -0
- package/dist/fields/text/inputSchema.d.ts +24 -0
- package/dist/fields/text/inputSchema.d.ts.map +1 -0
- package/dist/fields/text/types.d.ts +132 -0
- package/dist/fields/text/types.d.ts.map +1 -0
- package/dist/fields/text/validator.d.ts +39 -0
- package/dist/fields/text/validator.d.ts.map +1 -0
- package/dist/fields/types.d.ts +135 -0
- package/dist/fields/types.d.ts.map +1 -0
- package/dist/fields/upload/config.d.ts +24 -0
- package/dist/fields/upload/config.d.ts.map +1 -0
- package/dist/fields/upload/index.d.ts +5 -0
- package/dist/fields/upload/index.d.ts.map +1 -0
- package/dist/fields/upload/inputSchema.d.ts +17 -0
- package/dist/fields/upload/inputSchema.d.ts.map +1 -0
- package/dist/fields/upload/types.d.ts +78 -0
- package/dist/fields/upload/types.d.ts.map +1 -0
- package/dist/fields/upload/validator.d.ts +16 -0
- package/dist/fields/upload/validator.d.ts.map +1 -0
- package/dist/fields/url/config.d.ts +49 -0
- package/dist/fields/url/config.d.ts.map +1 -0
- package/dist/fields/url/index.d.ts +5 -0
- package/dist/fields/url/index.d.ts.map +1 -0
- package/dist/fields/url/inputSchema.d.ts +31 -0
- package/dist/fields/url/inputSchema.d.ts.map +1 -0
- package/dist/fields/url/types.d.ts +70 -0
- package/dist/fields/url/types.d.ts.map +1 -0
- package/dist/fields/url/validator.d.ts +37 -0
- package/dist/fields/url/validator.d.ts.map +1 -0
- package/dist/fields/utils.d.ts +47 -0
- package/dist/fields/utils.d.ts.map +1 -0
- package/dist/fields/validators/index.d.ts +23 -0
- package/dist/fields/validators/index.d.ts.map +1 -0
- package/dist/fields/validators/utils.d.ts +24 -0
- package/dist/fields/validators/utils.d.ts.map +1 -0
- package/dist/framework.d.ts +227 -0
- package/dist/framework.d.ts.map +1 -0
- package/dist/globals/config/config.d.ts +1 -0
- package/dist/globals/config/config.d.ts.map +1 -0
- package/dist/globals/config/types.d.ts +1 -0
- package/dist/globals/config/types.d.ts.map +1 -0
- package/dist/globals/config.d.ts +47 -0
- package/dist/globals/config.d.ts.map +1 -0
- package/dist/globals/index.d.ts +5 -0
- package/dist/globals/index.d.ts.map +1 -0
- package/dist/globals/interfaceGen.d.ts +33 -0
- package/dist/globals/interfaceGen.d.ts.map +1 -0
- package/dist/globals/types.d.ts +175 -0
- package/dist/globals/types.d.ts.map +1 -0
- package/dist/globals/utils.d.ts +37 -0
- package/dist/globals/utils.d.ts.map +1 -0
- package/dist/index.d.ts +15 -3165
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2101 -3470
- package/dist/index.js.map +1 -1
- package/dist/media/api/index.d.ts +5 -0
- package/dist/media/api/index.d.ts.map +1 -0
- package/dist/media/api/mutations.d.ts +76 -0
- package/dist/media/api/mutations.d.ts.map +1 -0
- package/dist/media/api/queries.d.ts +28 -0
- package/dist/media/api/queries.d.ts.map +1 -0
- package/dist/media/api/server.d.ts +68 -0
- package/dist/media/api/server.d.ts.map +1 -0
- package/dist/media/api/types.d.ts +171 -0
- package/dist/media/api/types.d.ts.map +1 -0
- package/dist/media/config.d.ts +25 -0
- package/dist/media/config.d.ts.map +1 -0
- package/dist/media/index.d.ts +6 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/types.d.ts +332 -0
- package/dist/media/types.d.ts.map +1 -0
- package/dist/media/utils.d.ts +35 -0
- package/dist/media/utils.d.ts.map +1 -0
- package/dist/media/validators.d.ts +61 -0
- package/dist/media/validators.d.ts.map +1 -0
- package/dist/schema/generateVexSchema.d.ts +36 -0
- package/dist/schema/generateVexSchema.d.ts.map +1 -0
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/migrate.d.ts +73 -0
- package/dist/schema/migrate.d.ts.map +1 -0
- package/dist/types/generateVexTypes.d.ts +41 -0
- package/dist/types/generateVexTypes.d.ts.map +1 -0
- package/dist/types/generated.d.ts +382 -0
- package/dist/types/generated.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils.d.ts +57 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +67 -24
|
@@ -0,0 +1,1877 @@
|
|
|
1
|
+
// src/api/server.ts
|
|
2
|
+
import {
|
|
3
|
+
paginationOptsValidator
|
|
4
|
+
} from "convex/server";
|
|
5
|
+
import { v } from "convex/values";
|
|
6
|
+
|
|
7
|
+
// src/api/depth.ts
|
|
8
|
+
function buildDepthPopulate(config, slug, depth, visited = /* @__PURE__ */ new Set()) {
|
|
9
|
+
if (depth <= 0) return {};
|
|
10
|
+
const source = config.collections.find((c) => c.slug === slug) ?? config.globals?.find((g) => g.slug === slug);
|
|
11
|
+
if (!source) return {};
|
|
12
|
+
const nextVisited = new Set(visited).add(slug);
|
|
13
|
+
const result = {};
|
|
14
|
+
for (const [key, field] of Object.entries(source.fields)) {
|
|
15
|
+
if (field.type !== "relationship") continue;
|
|
16
|
+
const targetSlug = field.collection.slug;
|
|
17
|
+
if (depth === 1 || nextVisited.has(targetSlug)) {
|
|
18
|
+
result[key] = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const nested = buildDepthPopulate(config, targetSlug, depth - 1, nextVisited);
|
|
22
|
+
result[key] = Object.keys(nested).length > 0 ? { populate: nested } : true;
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ../../node_modules/.pnpm/convex-helpers@0.1.120_@standard-schema+spec@1.1.0_convex@1.44.0_react@19.2.7__hono@4.1_fda37119e0b134c2babc8f4bd1faf097/node_modules/convex-helpers/index.js
|
|
28
|
+
async function asyncMap(list, asyncTransform) {
|
|
29
|
+
const promises = [];
|
|
30
|
+
let index = 0;
|
|
31
|
+
list = await list;
|
|
32
|
+
for (const item of list) {
|
|
33
|
+
promises.push(asyncTransform(item, index));
|
|
34
|
+
index += 1;
|
|
35
|
+
}
|
|
36
|
+
return Promise.all(promises);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ../../node_modules/.pnpm/convex-helpers@0.1.120_@standard-schema+spec@1.1.0_convex@1.44.0_react@19.2.7__hono@4.1_fda37119e0b134c2babc8f4bd1faf097/node_modules/convex-helpers/server/relationships.js
|
|
40
|
+
async function getAll(db, arg1, arg2) {
|
|
41
|
+
const [table, ids] = arg2 !== void 0 ? [arg1, arg2] : [null, arg1];
|
|
42
|
+
return table ? asyncMap(ids, (id) => db.get(table, id)) : asyncMap(ids, (id) => db.get(id));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/api/populate.ts
|
|
46
|
+
async function populateDocs(ctx, docs, populate) {
|
|
47
|
+
const result = await asyncMap(docs, async (doc) => {
|
|
48
|
+
const out = { ...doc };
|
|
49
|
+
for (const [fieldKey, opts] of Object.entries(populate)) {
|
|
50
|
+
const ids = doc[fieldKey];
|
|
51
|
+
if (!Array.isArray(ids)) continue;
|
|
52
|
+
const targets = await getAll(ctx.db, ids);
|
|
53
|
+
const filtered = targets.filter((t) => t !== null);
|
|
54
|
+
if (typeof opts === "object" && opts !== null && "populate" in opts && opts.populate && filtered.length > 0) {
|
|
55
|
+
out[fieldKey] = await populateDocs(ctx, filtered, opts.populate);
|
|
56
|
+
} else {
|
|
57
|
+
out[fieldKey] = filtered;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
});
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/access/constants.ts
|
|
66
|
+
var CRUD_ACTIONS = {
|
|
67
|
+
create: "create",
|
|
68
|
+
read: "read",
|
|
69
|
+
update: "update",
|
|
70
|
+
delete: "delete"
|
|
71
|
+
};
|
|
72
|
+
var DRAFT_ACTIONS = {
|
|
73
|
+
readDrafts: "readDrafts",
|
|
74
|
+
saveDraft: "saveDraft",
|
|
75
|
+
publish: "publish",
|
|
76
|
+
unpublish: "unpublish"
|
|
77
|
+
};
|
|
78
|
+
var QUERY_ACTIONS = {
|
|
79
|
+
read: CRUD_ACTIONS.read,
|
|
80
|
+
readDrafts: DRAFT_ACTIONS.readDrafts
|
|
81
|
+
};
|
|
82
|
+
var PERMISSION_MODES = {
|
|
83
|
+
allow: "allow",
|
|
84
|
+
deny: "deny"
|
|
85
|
+
};
|
|
86
|
+
var PERMISSION_SCOPES = {
|
|
87
|
+
doc: "doc",
|
|
88
|
+
any: "any",
|
|
89
|
+
all: "all"
|
|
90
|
+
};
|
|
91
|
+
var WILDCARD_KEY = "*";
|
|
92
|
+
var ADMIN_CUSTOM_SUBJECTS = {
|
|
93
|
+
adminPanel: {
|
|
94
|
+
key: "adminPanel",
|
|
95
|
+
actions: ["access", "impersonate"]
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// src/access/types.ts
|
|
100
|
+
import { ConvexError } from "convex/values";
|
|
101
|
+
function dataType() {
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
var VexAccessError = class extends ConvexError {
|
|
105
|
+
/** The subject on which access was denied. */
|
|
106
|
+
resource;
|
|
107
|
+
/** The denied action. */
|
|
108
|
+
action;
|
|
109
|
+
/**
|
|
110
|
+
* @param options — Structured denial context.
|
|
111
|
+
* @param options.message — Human-readable error message.
|
|
112
|
+
* @param options.resource — Subject name.
|
|
113
|
+
* @param options.action — Action name.
|
|
114
|
+
*/
|
|
115
|
+
constructor(options) {
|
|
116
|
+
super({
|
|
117
|
+
code: "ACCESS_DENIED",
|
|
118
|
+
resource: options.resource,
|
|
119
|
+
action: options.action,
|
|
120
|
+
message: options.message ?? `Access Denied: ${options.resource}/${options.action}`
|
|
121
|
+
});
|
|
122
|
+
this.name = "VexAccessError";
|
|
123
|
+
this.resource = options.resource;
|
|
124
|
+
this.action = options.action;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var VexAccessConfigError = class extends Error {
|
|
128
|
+
/** @param message — Human-readable description of the configuration error. */
|
|
129
|
+
constructor(message) {
|
|
130
|
+
super(message);
|
|
131
|
+
this.name = "VexAccessConfigError";
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// src/access/createFilterConstraintBuilder.ts
|
|
136
|
+
function createFilterConstraintBuilder() {
|
|
137
|
+
const asFilterConstraint = (result) => result;
|
|
138
|
+
const compare = (op) => (field, value) => ({
|
|
139
|
+
kind: "compare",
|
|
140
|
+
field,
|
|
141
|
+
op,
|
|
142
|
+
value
|
|
143
|
+
});
|
|
144
|
+
const builder = {
|
|
145
|
+
eq: compare("eq"),
|
|
146
|
+
neq: compare("neq"),
|
|
147
|
+
gt: compare("gt"),
|
|
148
|
+
gte: compare("gte"),
|
|
149
|
+
lt: compare("lt"),
|
|
150
|
+
lte: compare("lte"),
|
|
151
|
+
and: (...nodes) => ({ kind: "and", nodes: nodes.map(asFilterConstraint) }),
|
|
152
|
+
or: (...nodes) => ({ kind: "or", nodes: nodes.map(asFilterConstraint) }),
|
|
153
|
+
not: (node) => ({ kind: "not", node: asFilterConstraint(node) })
|
|
154
|
+
};
|
|
155
|
+
return builder;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/access/createIndexConstraintBuilder.ts
|
|
159
|
+
var CONSTRAINTS = /* @__PURE__ */ Symbol("vex.access.indexConstraints");
|
|
160
|
+
function createIndexConstraintBuilder() {
|
|
161
|
+
function node(constraints) {
|
|
162
|
+
const extend = (op, field, value) => node([
|
|
163
|
+
...constraints,
|
|
164
|
+
{ field, op, value }
|
|
165
|
+
]);
|
|
166
|
+
return {
|
|
167
|
+
eq: (field, value) => extend("eq", field, value),
|
|
168
|
+
gt: (field, value) => extend("gt", field, value),
|
|
169
|
+
gte: (field, value) => extend("gte", field, value),
|
|
170
|
+
lt: (field, value) => extend("lt", field, value),
|
|
171
|
+
lte: (field, value) => extend("lte", field, value),
|
|
172
|
+
[CONSTRAINTS]: constraints
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return node([]);
|
|
176
|
+
}
|
|
177
|
+
function readIndexConstraints(result) {
|
|
178
|
+
const carried = result[CONSTRAINTS];
|
|
179
|
+
return carried ?? [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/access/createAccessQueryBuilder.ts
|
|
183
|
+
var CONDITION = /* @__PURE__ */ Symbol("vex.access.condition");
|
|
184
|
+
function createAccessQueryBuilder() {
|
|
185
|
+
const conditionNode = (condition) => ({
|
|
186
|
+
[CONDITION]: condition
|
|
187
|
+
});
|
|
188
|
+
const runFilter = (predicate) => predicate(createFilterConstraintBuilder());
|
|
189
|
+
const builder = {
|
|
190
|
+
withIndex: (name, range) => {
|
|
191
|
+
const built = range(createIndexConstraintBuilder());
|
|
192
|
+
const constraints = readIndexConstraints(built);
|
|
193
|
+
const base = { index: { name, constraints } };
|
|
194
|
+
return {
|
|
195
|
+
...conditionNode(base),
|
|
196
|
+
// Continuing with `.filter(…)` produces a NEW condition carrying both
|
|
197
|
+
// halves; the index-only value the rule already holds is unchanged.
|
|
198
|
+
filter: (predicate) => conditionNode({ ...base, filter: runFilter(predicate) })
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
filter: (predicate) => conditionNode({ filter: runFilter(predicate) })
|
|
202
|
+
};
|
|
203
|
+
return builder;
|
|
204
|
+
}
|
|
205
|
+
function readAccessCondition(result) {
|
|
206
|
+
if (result === null || typeof result !== "object" && typeof result !== "function") {
|
|
207
|
+
return void 0;
|
|
208
|
+
}
|
|
209
|
+
return result[CONDITION];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/access/compileConstraints.ts
|
|
213
|
+
function convexValuesEqual(a, b) {
|
|
214
|
+
if (a === b) return true;
|
|
215
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
216
|
+
if (a instanceof ArrayBuffer || b instanceof ArrayBuffer) {
|
|
217
|
+
if (!(a instanceof ArrayBuffer) || !(b instanceof ArrayBuffer)) return false;
|
|
218
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
219
|
+
const left2 = new Uint8Array(a);
|
|
220
|
+
const right2 = new Uint8Array(b);
|
|
221
|
+
return left2.every((byte, i) => byte === right2[i]);
|
|
222
|
+
}
|
|
223
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
224
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
225
|
+
return a.every((item, i) => convexValuesEqual(item, b[i]));
|
|
226
|
+
}
|
|
227
|
+
const left = a;
|
|
228
|
+
const right = b;
|
|
229
|
+
const leftKeys = Object.keys(left);
|
|
230
|
+
if (leftKeys.length !== Object.keys(right).length) return false;
|
|
231
|
+
return leftKeys.every(
|
|
232
|
+
(key) => key in right && convexValuesEqual(left[key], right[key])
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
var CONSTRAINT_COMPARATORS = {
|
|
236
|
+
eq: convexValuesEqual,
|
|
237
|
+
gt: (a, b) => a > b,
|
|
238
|
+
gte: (a, b) => a >= b,
|
|
239
|
+
lt: (a, b) => a < b,
|
|
240
|
+
lte: (a, b) => a <= b
|
|
241
|
+
};
|
|
242
|
+
function accessConstraintsToIndexRange(props) {
|
|
243
|
+
return (q) => props.constraints.reduce(
|
|
244
|
+
(builder, c) => (
|
|
245
|
+
// Convex's `IndexRangeBuilder` narrows its RETURN TYPE per call, exactly as
|
|
246
|
+
// `ConstraintBuilder` does — so a dynamic reduce over an untyped op string
|
|
247
|
+
// needs one cast at this boundary. The static safety already happened when
|
|
248
|
+
// the rule's `constraints` callback was type-checked, and again at
|
|
249
|
+
// `defineAccess` time in `validateAccessConstraints`.
|
|
250
|
+
builder[c.op](
|
|
251
|
+
c.field,
|
|
252
|
+
c.value
|
|
253
|
+
)
|
|
254
|
+
),
|
|
255
|
+
q
|
|
256
|
+
// Every `IndexRangeBuilder` structurally extends `IndexRange`, including one
|
|
257
|
+
// with zero calls applied — so an empty constraint list returns `q` itself,
|
|
258
|
+
// matching Convex's own "no restriction" semantics.
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
function accessConstraintsToFilter(props) {
|
|
262
|
+
const q = props.q;
|
|
263
|
+
const [first, ...rest] = props.constraints.map((c) => q[c.op](q.field(c.field), c.value));
|
|
264
|
+
if (first === void 0) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
"accessConstraintsToFilter: `constraints` must be non-empty \u2014 an unrestricted rule is resolved to a boolean before it reaches a compile target."
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
return rest.length === 0 ? first : q.and(first, ...rest);
|
|
270
|
+
}
|
|
271
|
+
function accessConstraintsToPredicate(props) {
|
|
272
|
+
return (doc) => props.constraints.every((c) => CONSTRAINT_COMPARATORS[c.op](doc[c.field], c.value));
|
|
273
|
+
}
|
|
274
|
+
var FILTER_COMPARATORS = {
|
|
275
|
+
...CONSTRAINT_COMPARATORS,
|
|
276
|
+
neq: (a, b) => !convexValuesEqual(a, b)
|
|
277
|
+
};
|
|
278
|
+
function accessFilterTreeToFilter(props) {
|
|
279
|
+
const q = props.q;
|
|
280
|
+
function compile(node) {
|
|
281
|
+
switch (node.kind) {
|
|
282
|
+
case "compare":
|
|
283
|
+
return q[node.op](q.field(node.field), node.value);
|
|
284
|
+
case "and":
|
|
285
|
+
return q.and(...node.nodes.map(compile));
|
|
286
|
+
case "or":
|
|
287
|
+
return q.or(...node.nodes.map(compile));
|
|
288
|
+
case "not":
|
|
289
|
+
return q.not(compile(node.node));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return compile(props.node);
|
|
293
|
+
}
|
|
294
|
+
function accessFilterTreeToPredicate(props) {
|
|
295
|
+
function evaluate(node, doc) {
|
|
296
|
+
switch (node.kind) {
|
|
297
|
+
case "compare":
|
|
298
|
+
return FILTER_COMPARATORS[node.op](doc[node.field], node.value);
|
|
299
|
+
case "and":
|
|
300
|
+
return node.nodes.every((child) => evaluate(child, doc));
|
|
301
|
+
case "or":
|
|
302
|
+
return node.nodes.some((child) => evaluate(child, doc));
|
|
303
|
+
case "not":
|
|
304
|
+
return !evaluate(node.node, doc);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return (doc) => evaluate(props.node, doc);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/access/hasPermission.ts
|
|
311
|
+
function hasPermission(props) {
|
|
312
|
+
const { access } = props;
|
|
313
|
+
if (!access || access === void 0 || !access.enabled) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
const rawRoles = props.user ? props.user[access.userRolesField] : [];
|
|
317
|
+
const userRoles = typeof rawRoles === "string" ? [rawRoles] : Array.isArray(rawRoles) ? rawRoles.filter((role) => typeof role === "string") : [];
|
|
318
|
+
const effectiveRoles = userRoles.length === 0 && access.anonRole !== void 0 ? [access.anonRole] : userRoles;
|
|
319
|
+
const knownRoles = effectiveRoles.filter((role) => access.roles.includes(role));
|
|
320
|
+
const defaultAllowed = access.defaultPermissionMode === PERMISSION_MODES.allow;
|
|
321
|
+
let allPermissions;
|
|
322
|
+
if (knownRoles.length === 0) {
|
|
323
|
+
allPermissions = false;
|
|
324
|
+
} else {
|
|
325
|
+
const resolved = knownRoles.map((userRole) => {
|
|
326
|
+
const role = access.permissions[userRole];
|
|
327
|
+
const resource = role?.[props.resource];
|
|
328
|
+
let check;
|
|
329
|
+
if (typeof resource === "boolean") {
|
|
330
|
+
check = resource;
|
|
331
|
+
} else if (resource !== null && resource !== void 0 && typeof resource === "object") {
|
|
332
|
+
check = resolveActionCheck({
|
|
333
|
+
resource,
|
|
334
|
+
action: props.action
|
|
335
|
+
}) ?? defaultAllowed;
|
|
336
|
+
} else {
|
|
337
|
+
const roleWildcard = role?.[WILDCARD_KEY];
|
|
338
|
+
check = typeof roleWildcard === "boolean" ? roleWildcard : defaultAllowed;
|
|
339
|
+
}
|
|
340
|
+
return resolvePermissionCheck({
|
|
341
|
+
check,
|
|
342
|
+
user: props.user,
|
|
343
|
+
data: props.data,
|
|
344
|
+
organization: access.orgCollectionSlug !== void 0 ? props.organization : void 0,
|
|
345
|
+
resource: props.resource,
|
|
346
|
+
action: props.action,
|
|
347
|
+
scope: props.scope ?? PERMISSION_SCOPES.all
|
|
348
|
+
}) ?? defaultAllowed;
|
|
349
|
+
});
|
|
350
|
+
allPermissions = resolved.some(Boolean);
|
|
351
|
+
}
|
|
352
|
+
if (allPermissions === false) {
|
|
353
|
+
if (props.throwOnDenied) {
|
|
354
|
+
throw new VexAccessError({
|
|
355
|
+
resource: props.resource,
|
|
356
|
+
action: props.action
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
var CAPABILITY_PROBE = /* @__PURE__ */ Symbol("vex.capabilityProbe");
|
|
364
|
+
function isConstrainedCheck(check) {
|
|
365
|
+
return typeof check === "object" && check !== null && "constraints" in check;
|
|
366
|
+
}
|
|
367
|
+
function resolveConstrainedCheck(props) {
|
|
368
|
+
const outcome = props.check.constraints({
|
|
369
|
+
user: props.user,
|
|
370
|
+
q: createAccessQueryBuilder(),
|
|
371
|
+
...props.organization !== void 0 ? { organization: props.organization } : {}
|
|
372
|
+
});
|
|
373
|
+
if (typeof outcome === "boolean") return outcome;
|
|
374
|
+
if (props.data === void 0) {
|
|
375
|
+
if (props.scope === PERMISSION_SCOPES.any) return true;
|
|
376
|
+
if (props.scope === PERMISSION_SCOPES.all) return false;
|
|
377
|
+
throw new VexAccessError({
|
|
378
|
+
resource: props.resource,
|
|
379
|
+
action: props.action,
|
|
380
|
+
message: `hasPermission: "${props.resource}.${props.action}" needs a "data" object (its check is a constraint). Pass "data" for an exact check, or use scope: "any" (nav/list gating) or scope: "all" (bulk actions).`
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
const condition = readAccessCondition(outcome);
|
|
384
|
+
if (condition === void 0) return false;
|
|
385
|
+
const doc = props.data;
|
|
386
|
+
const indexHolds = condition.index === void 0 || accessConstraintsToPredicate({ constraints: condition.index.constraints })(doc);
|
|
387
|
+
const filterHolds = condition.filter === void 0 || accessFilterTreeToPredicate({ node: condition.filter })(doc);
|
|
388
|
+
if (!indexHolds || !filterHolds) return false;
|
|
389
|
+
if (props.check.filter === void 0) return true;
|
|
390
|
+
return resolvePermissionCheck({
|
|
391
|
+
...props,
|
|
392
|
+
check: props.check.filter
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function resolvePermissionCheck(props) {
|
|
396
|
+
if (isConstrainedCheck(props.check)) {
|
|
397
|
+
return resolveConstrainedCheck({
|
|
398
|
+
...props,
|
|
399
|
+
check: props.check
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
if (typeof props.check !== "function") {
|
|
403
|
+
return props.check;
|
|
404
|
+
}
|
|
405
|
+
const callbackProps = {
|
|
406
|
+
user: props.user,
|
|
407
|
+
data: props.data !== void 0 ? props.data : void 0,
|
|
408
|
+
organization: props.organization !== void 0 ? props.organization : void 0
|
|
409
|
+
};
|
|
410
|
+
if (props.data !== void 0) {
|
|
411
|
+
const result = props.check(callbackProps);
|
|
412
|
+
return result === void 0 ? false : result;
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
const probe = new Proxy(
|
|
416
|
+
{},
|
|
417
|
+
{
|
|
418
|
+
get() {
|
|
419
|
+
throw { [CAPABILITY_PROBE]: true };
|
|
420
|
+
},
|
|
421
|
+
has() {
|
|
422
|
+
throw { [CAPABILITY_PROBE]: true };
|
|
423
|
+
},
|
|
424
|
+
ownKeys() {
|
|
425
|
+
throw { [CAPABILITY_PROBE]: true };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
const result = props.check({ ...callbackProps, data: probe });
|
|
430
|
+
return result === void 0 ? false : result;
|
|
431
|
+
} catch (e) {
|
|
432
|
+
const touchedData = typeof e === "object" && e !== null && e[CAPABILITY_PROBE] === true;
|
|
433
|
+
if (!touchedData) throw e;
|
|
434
|
+
if (props.scope === PERMISSION_SCOPES.any) {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
if (props.scope === PERMISSION_SCOPES.all) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
throw new VexAccessError({
|
|
441
|
+
resource: props.resource,
|
|
442
|
+
action: props.action,
|
|
443
|
+
message: `hasPermission: "${props.resource}.${props.action}" needs a "data" object (its check reads the document). Pass "data" for an exact check, or use scope: "any" (nav/list gating) or scope: "all" (bulk actions).`
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function resolveActionCheck(props) {
|
|
448
|
+
if (props.action in props.resource) {
|
|
449
|
+
return props.resource[props.action];
|
|
450
|
+
}
|
|
451
|
+
if (WILDCARD_KEY in props.resource) {
|
|
452
|
+
return props.resource[WILDCARD_KEY];
|
|
453
|
+
}
|
|
454
|
+
return void 0;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/access/resolveAccessRule.ts
|
|
458
|
+
function isConstrainedCheck2(check) {
|
|
459
|
+
return typeof check === "object" && check !== null && "constraints" in check;
|
|
460
|
+
}
|
|
461
|
+
function classifyRole(props) {
|
|
462
|
+
const defaultAllowed = props.access.defaultPermissionMode === PERMISSION_MODES.allow;
|
|
463
|
+
const roleRules = props.access.permissions[props.role];
|
|
464
|
+
const subject = roleRules?.[props.resource];
|
|
465
|
+
let check;
|
|
466
|
+
if (typeof subject === "boolean") {
|
|
467
|
+
check = subject;
|
|
468
|
+
} else if (subject !== null && subject !== void 0 && typeof subject === "object") {
|
|
469
|
+
check = resolveActionCheck({
|
|
470
|
+
resource: subject,
|
|
471
|
+
action: props.action
|
|
472
|
+
}) ?? defaultAllowed;
|
|
473
|
+
} else {
|
|
474
|
+
const roleWildcard = roleRules?.[WILDCARD_KEY];
|
|
475
|
+
check = typeof roleWildcard === "boolean" ? roleWildcard : defaultAllowed;
|
|
476
|
+
}
|
|
477
|
+
if (check === false) return { kind: "deny" };
|
|
478
|
+
if (check === true) return { kind: "unrestricted" };
|
|
479
|
+
if (!isConstrainedCheck2(check)) return { kind: "opaque" };
|
|
480
|
+
const outcome = check.constraints({
|
|
481
|
+
user: props.user ?? {},
|
|
482
|
+
q: createAccessQueryBuilder(),
|
|
483
|
+
...props.access.orgCollectionSlug !== void 0 ? { organization: props.organization } : {}
|
|
484
|
+
});
|
|
485
|
+
if (outcome === true) return { kind: "unrestricted" };
|
|
486
|
+
if (outcome === false) return { kind: "deny" };
|
|
487
|
+
const condition = readAccessCondition(outcome);
|
|
488
|
+
if (condition === void 0) return { kind: "opaque" };
|
|
489
|
+
return { kind: "conditional", condition };
|
|
490
|
+
}
|
|
491
|
+
function selectSingleCondition(props) {
|
|
492
|
+
const { access } = props;
|
|
493
|
+
const rawRoles = props.user ? props.user[access.userRolesField] : [];
|
|
494
|
+
const userRoles = typeof rawRoles === "string" ? [rawRoles] : Array.isArray(rawRoles) ? rawRoles.filter((role) => typeof role === "string") : [];
|
|
495
|
+
const effectiveRoles = userRoles.length === 0 && access.anonRole !== void 0 ? [access.anonRole] : userRoles;
|
|
496
|
+
const knownRoles = effectiveRoles.filter((role) => access.roles.includes(role));
|
|
497
|
+
if (knownRoles.length === 0) return void 0;
|
|
498
|
+
const candidates = [];
|
|
499
|
+
for (const role of knownRoles) {
|
|
500
|
+
const contribution = classifyRole({
|
|
501
|
+
access,
|
|
502
|
+
role,
|
|
503
|
+
resource: props.resource,
|
|
504
|
+
action: props.action,
|
|
505
|
+
user: props.user,
|
|
506
|
+
organization: props.organization
|
|
507
|
+
});
|
|
508
|
+
if (contribution.kind === "deny") continue;
|
|
509
|
+
if (contribution.kind === "unrestricted" || contribution.kind === "opaque") return void 0;
|
|
510
|
+
candidates.push(contribution.condition);
|
|
511
|
+
}
|
|
512
|
+
if (candidates.length !== 1) return void 0;
|
|
513
|
+
return candidates[0];
|
|
514
|
+
}
|
|
515
|
+
function resolveAccessIndex(props) {
|
|
516
|
+
const access = props.access;
|
|
517
|
+
if (!access || !access.enabled) return void 0;
|
|
518
|
+
const condition = selectSingleCondition({
|
|
519
|
+
access,
|
|
520
|
+
user: props.user,
|
|
521
|
+
organization: props.organization,
|
|
522
|
+
resource: props.resource,
|
|
523
|
+
action: props.action
|
|
524
|
+
});
|
|
525
|
+
if (condition?.index === void 0) return void 0;
|
|
526
|
+
return {
|
|
527
|
+
name: condition.index.name,
|
|
528
|
+
range: accessConstraintsToIndexRange({ constraints: condition.index.constraints })
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function resolveAccessConstraint(props) {
|
|
532
|
+
const access = props.access;
|
|
533
|
+
if (!access || !access.enabled) return void 0;
|
|
534
|
+
const condition = selectSingleCondition({
|
|
535
|
+
access,
|
|
536
|
+
user: props.user,
|
|
537
|
+
organization: props.organization,
|
|
538
|
+
resource: props.resource,
|
|
539
|
+
action: props.action
|
|
540
|
+
});
|
|
541
|
+
if (condition === void 0) return void 0;
|
|
542
|
+
const { index, filter } = condition;
|
|
543
|
+
const indexConstrains = props.indexAlreadyApplied !== true && index !== void 0 && index.constraints.length > 0;
|
|
544
|
+
if (indexConstrains && filter !== void 0) {
|
|
545
|
+
return (q) => q.and(
|
|
546
|
+
accessConstraintsToFilter({ constraints: index.constraints, q }),
|
|
547
|
+
accessFilterTreeToFilter({ node: filter, q })
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
if (indexConstrains) {
|
|
551
|
+
return (q) => accessConstraintsToFilter({ constraints: index.constraints, q });
|
|
552
|
+
}
|
|
553
|
+
if (filter !== void 0) {
|
|
554
|
+
return (q) => accessFilterTreeToFilter({ node: filter, q });
|
|
555
|
+
}
|
|
556
|
+
return void 0;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/access/pickQueryIndex.ts
|
|
560
|
+
var warnedDisplacements = /* @__PURE__ */ new Set();
|
|
561
|
+
function warnDisplacedAccessIndex(props) {
|
|
562
|
+
if (process.env.NODE_ENV === "production") return;
|
|
563
|
+
const key = `${props.accessIndexName}\0${props.callerIndexName}`;
|
|
564
|
+
if (warnedDisplacements.has(key)) return;
|
|
565
|
+
warnedDisplacements.add(key);
|
|
566
|
+
console.warn(
|
|
567
|
+
`[vexcms] Access index "${props.accessIndexName}" cannot be applied: this query already uses "${props.callerIndexName}". Convex permits one index per query, so the access rule falls back to a per-document check and reads scale with the caller's range instead. Declare a compound index covering both fields to serve them in one query.`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
function composeRanges(props) {
|
|
571
|
+
const { first, second } = props;
|
|
572
|
+
if (!first) return second;
|
|
573
|
+
if (!second) return first;
|
|
574
|
+
return (q) => second(first(q));
|
|
575
|
+
}
|
|
576
|
+
function pickQueryIndex(props) {
|
|
577
|
+
const { accessIndex, callerIndex } = props;
|
|
578
|
+
if (!accessIndex && !callerIndex) return void 0;
|
|
579
|
+
if (!callerIndex) return accessIndex;
|
|
580
|
+
if (!accessIndex) return callerIndex;
|
|
581
|
+
if (accessIndex.name === callerIndex.name) {
|
|
582
|
+
return {
|
|
583
|
+
name: accessIndex.name,
|
|
584
|
+
range: composeRanges({ first: accessIndex.range, second: callerIndex.range })
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
warnDisplacedAccessIndex({
|
|
588
|
+
accessIndexName: accessIndex.name,
|
|
589
|
+
callerIndexName: callerIndex.name
|
|
590
|
+
});
|
|
591
|
+
return callerIndex;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/fields/constants.ts
|
|
595
|
+
var ADMIN_FIELDS = {
|
|
596
|
+
text: {
|
|
597
|
+
type: "text",
|
|
598
|
+
interfaceType: "string",
|
|
599
|
+
validator: "v.string()",
|
|
600
|
+
defaultValue: ""
|
|
601
|
+
},
|
|
602
|
+
url: {
|
|
603
|
+
type: "url",
|
|
604
|
+
interfaceType: "string",
|
|
605
|
+
validator: "v.string()",
|
|
606
|
+
defaultValue: ""
|
|
607
|
+
},
|
|
608
|
+
color: {
|
|
609
|
+
type: "color",
|
|
610
|
+
interfaceType: "string",
|
|
611
|
+
validator: "v.string()",
|
|
612
|
+
defaultValue: ""
|
|
613
|
+
},
|
|
614
|
+
number: {
|
|
615
|
+
type: "number",
|
|
616
|
+
interfaceType: "number",
|
|
617
|
+
validator: "v.number()",
|
|
618
|
+
defaultValue: 0
|
|
619
|
+
},
|
|
620
|
+
checkbox: {
|
|
621
|
+
type: "checkbox",
|
|
622
|
+
interfaceType: "boolean",
|
|
623
|
+
validator: "v.boolean()",
|
|
624
|
+
defaultValue: false
|
|
625
|
+
},
|
|
626
|
+
date: {
|
|
627
|
+
type: "date",
|
|
628
|
+
interfaceType: "number",
|
|
629
|
+
validator: "v.number()",
|
|
630
|
+
defaultValue: void 0
|
|
631
|
+
},
|
|
632
|
+
select: {
|
|
633
|
+
type: "select",
|
|
634
|
+
interfaceType: "string[]",
|
|
635
|
+
validator: "v.array(\nv.string()\n)",
|
|
636
|
+
defaultValue: []
|
|
637
|
+
},
|
|
638
|
+
relationship: {
|
|
639
|
+
type: "relationship",
|
|
640
|
+
interfaceType: "Id<CollectionSlug>[]",
|
|
641
|
+
validator: "v.array(\nv.string()\n)",
|
|
642
|
+
defaultValue: []
|
|
643
|
+
},
|
|
644
|
+
array: {
|
|
645
|
+
type: "array",
|
|
646
|
+
interfaceType: "unknown[]",
|
|
647
|
+
validator: "v.array(\nv.any()\n)",
|
|
648
|
+
defaultValue: []
|
|
649
|
+
},
|
|
650
|
+
group: {
|
|
651
|
+
type: "group",
|
|
652
|
+
interfaceType: "Record<string, any>",
|
|
653
|
+
validator: "v.object({})",
|
|
654
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
655
|
+
defaultValue: {}
|
|
656
|
+
},
|
|
657
|
+
blocks: {
|
|
658
|
+
type: "blocks",
|
|
659
|
+
interfaceType: "Record<string, any>[]",
|
|
660
|
+
// placeholder - blocks() overridses per instance
|
|
661
|
+
validator: "v.array(v.any())",
|
|
662
|
+
// placeholder - blocksFieldToValidator builds dynamically
|
|
663
|
+
defaultValue: []
|
|
664
|
+
},
|
|
665
|
+
upload: {
|
|
666
|
+
type: "upload",
|
|
667
|
+
interfaceType: "Id<MediaCollectionSlug>[]",
|
|
668
|
+
validator: "v.array(\nv.string()\n)",
|
|
669
|
+
defaultValue: void 0
|
|
670
|
+
}
|
|
671
|
+
// richtext: {
|
|
672
|
+
// type: "richtext",
|
|
673
|
+
// validator: "v.any()",
|
|
674
|
+
// defaultValue: {},
|
|
675
|
+
// },
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
// src/api/utils.ts
|
|
679
|
+
function resolveAccessCall(props) {
|
|
680
|
+
const configured = props.config?.access;
|
|
681
|
+
const bypassed = props.access?.bypass === true;
|
|
682
|
+
const action = props.access?.action ?? props.defaultAction;
|
|
683
|
+
if (process.env.NODE_ENV !== "production") {
|
|
684
|
+
if (bypassed && configured === void 0) {
|
|
685
|
+
console.warn(
|
|
686
|
+
`[vexcms] access.bypass was set for "${props.resource}" but this call has no access config, so RBAC was already off and the flag changed nothing. Either drop it, or check that the call receives the config you expect.`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
if (!bypassed && props.access?.action !== void 0 && configured !== void 0) {
|
|
690
|
+
const declaredForResource = configured.customActions?.[props.resource];
|
|
691
|
+
const declared = /* @__PURE__ */ new Set([
|
|
692
|
+
...Object.values(CRUD_ACTIONS),
|
|
693
|
+
...Object.values(DRAFT_ACTIONS),
|
|
694
|
+
...declaredForResource?.query ?? [],
|
|
695
|
+
...declaredForResource?.mutation ?? []
|
|
696
|
+
]);
|
|
697
|
+
if (!declared.has(action)) {
|
|
698
|
+
console.warn(
|
|
699
|
+
`[vexcms] Action "${action}" is not declared for "${props.resource}": it is neither a built-in verb nor listed in customActions["${props.resource}"]. No rule can name it, so this call resolves to a denial. Check for a typo.`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return { access: bypassed ? void 0 : configured, action, resource: props.resource };
|
|
705
|
+
}
|
|
706
|
+
function resolveCollectionSlug(props) {
|
|
707
|
+
for (const c of props.config?.collections ?? []) {
|
|
708
|
+
if (props.ctx.db.normalizeId(c.slug, props.id) !== null) {
|
|
709
|
+
return c.slug;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
throw new Error("[resolveCollectionSlug]: document id does not match a collection slug");
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/api/find/server.ts
|
|
716
|
+
async function find(args) {
|
|
717
|
+
const { access, action, resource } = resolveAccessCall({
|
|
718
|
+
config: args.config,
|
|
719
|
+
access: args.access,
|
|
720
|
+
defaultAction: CRUD_ACTIONS.read,
|
|
721
|
+
resource: args.collection
|
|
722
|
+
});
|
|
723
|
+
const accessIndex = resolveAccessIndex({
|
|
724
|
+
access,
|
|
725
|
+
user: args.auth?.user ?? null,
|
|
726
|
+
organization: args.auth?.organization,
|
|
727
|
+
resource,
|
|
728
|
+
action
|
|
729
|
+
});
|
|
730
|
+
const callerIndex = args.withIndex;
|
|
731
|
+
const resolvedIndex = pickQueryIndex({ accessIndex, callerIndex });
|
|
732
|
+
const accessFilter = resolveAccessConstraint({
|
|
733
|
+
access,
|
|
734
|
+
user: args.auth?.user ?? null,
|
|
735
|
+
organization: args.auth?.organization,
|
|
736
|
+
resource,
|
|
737
|
+
action,
|
|
738
|
+
indexAlreadyApplied: accessIndex !== void 0 && resolvedIndex?.name === accessIndex.name
|
|
739
|
+
});
|
|
740
|
+
const findQuery = buildQuery({
|
|
741
|
+
...args,
|
|
742
|
+
resolvedIndex,
|
|
743
|
+
accessFilter
|
|
744
|
+
});
|
|
745
|
+
let docs;
|
|
746
|
+
let convexPaginationResult;
|
|
747
|
+
if (args.paginationOpts) {
|
|
748
|
+
convexPaginationResult = await findQuery.paginate(args.paginationOpts);
|
|
749
|
+
docs = convexPaginationResult.page.filter(
|
|
750
|
+
(d) => hasPermission({
|
|
751
|
+
access,
|
|
752
|
+
resource,
|
|
753
|
+
action,
|
|
754
|
+
data: d,
|
|
755
|
+
user: args.auth?.user ?? {},
|
|
756
|
+
organization: args.auth?.organization
|
|
757
|
+
})
|
|
758
|
+
);
|
|
759
|
+
} else if (args.limit) {
|
|
760
|
+
docs = (await findQuery.take(args.limit)).filter(
|
|
761
|
+
(d) => hasPermission({
|
|
762
|
+
access,
|
|
763
|
+
resource,
|
|
764
|
+
action,
|
|
765
|
+
data: d,
|
|
766
|
+
user: args.auth?.user ?? {},
|
|
767
|
+
organization: args.auth?.organization
|
|
768
|
+
})
|
|
769
|
+
);
|
|
770
|
+
} else {
|
|
771
|
+
docs = (await findQuery.collect()).filter(
|
|
772
|
+
(d) => hasPermission({
|
|
773
|
+
access,
|
|
774
|
+
resource,
|
|
775
|
+
action,
|
|
776
|
+
data: d,
|
|
777
|
+
user: args.auth?.user ?? {},
|
|
778
|
+
organization: args.auth?.organization
|
|
779
|
+
})
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
const effectivePopulate = args.populate ?? (args.depth !== void 0 && args.depth > 0 && args.config ? buildDepthPopulate(args.config, args.collection, args.depth) : void 0);
|
|
783
|
+
const finalDocs = !effectivePopulate || Object.keys(effectivePopulate).length === 0 ? docs : await populateDocs(args.ctx, docs, effectivePopulate);
|
|
784
|
+
if (args.paginationOpts && convexPaginationResult) {
|
|
785
|
+
if (args.paginationOpts.totalDocs && !args.paginationOpts.cursor) {
|
|
786
|
+
try {
|
|
787
|
+
if (convexPaginationResult.isDone) {
|
|
788
|
+
return {
|
|
789
|
+
...convexPaginationResult,
|
|
790
|
+
page: finalDocs,
|
|
791
|
+
totalDocs: finalDocs.length
|
|
792
|
+
};
|
|
793
|
+
} else {
|
|
794
|
+
const countQuery = buildQuery({
|
|
795
|
+
...args,
|
|
796
|
+
resolvedIndex,
|
|
797
|
+
accessFilter
|
|
798
|
+
});
|
|
799
|
+
const totalDocs = (await countQuery.collect()).filter(
|
|
800
|
+
(d) => hasPermission({
|
|
801
|
+
access,
|
|
802
|
+
resource,
|
|
803
|
+
action,
|
|
804
|
+
data: d,
|
|
805
|
+
user: args.auth?.user ?? {},
|
|
806
|
+
organization: args.auth?.organization
|
|
807
|
+
})
|
|
808
|
+
);
|
|
809
|
+
return {
|
|
810
|
+
...convexPaginationResult,
|
|
811
|
+
page: finalDocs,
|
|
812
|
+
totalDocs: totalDocs.length
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
} catch (error) {
|
|
816
|
+
console.warn("Failed to collect all documents:", error);
|
|
817
|
+
return {
|
|
818
|
+
...convexPaginationResult,
|
|
819
|
+
page: finalDocs,
|
|
820
|
+
totalDocs: null
|
|
821
|
+
// Signals "too large to count"
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return {
|
|
826
|
+
...convexPaginationResult,
|
|
827
|
+
page: finalDocs
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
return finalDocs;
|
|
831
|
+
}
|
|
832
|
+
function buildQuery(props) {
|
|
833
|
+
const tableName = props.collection;
|
|
834
|
+
let q = props.ctx.db.query(tableName);
|
|
835
|
+
if (props.resolvedIndex) {
|
|
836
|
+
q = props.resolvedIndex.range ? q.withIndex(props.resolvedIndex.name, props.resolvedIndex.range) : q.withIndex(props.resolvedIndex.name);
|
|
837
|
+
}
|
|
838
|
+
if (props.order) q = q.order(props.order);
|
|
839
|
+
if (props.accessFilter && props.filter) {
|
|
840
|
+
q = q.filter((filterQ) => filterQ.and(props.accessFilter(filterQ), props.filter(filterQ)));
|
|
841
|
+
} else if (props.accessFilter) {
|
|
842
|
+
q = q.filter(props.accessFilter);
|
|
843
|
+
} else if (props.filter) {
|
|
844
|
+
q = q.filter(props.filter);
|
|
845
|
+
}
|
|
846
|
+
return q;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/api/get/server.ts
|
|
850
|
+
async function get(args) {
|
|
851
|
+
const doc = await args.ctx.db.get(args.id);
|
|
852
|
+
if (doc && args.config?.access !== void 0) {
|
|
853
|
+
const { access, action, resource } = resolveAccessCall({
|
|
854
|
+
config: args.config,
|
|
855
|
+
access: args.access,
|
|
856
|
+
defaultAction: CRUD_ACTIONS.read,
|
|
857
|
+
resource: args.collection
|
|
858
|
+
});
|
|
859
|
+
hasPermission({
|
|
860
|
+
throwOnDenied: true,
|
|
861
|
+
access,
|
|
862
|
+
user: args.auth?.user ?? {},
|
|
863
|
+
organization: args.auth?.organization,
|
|
864
|
+
resource,
|
|
865
|
+
action,
|
|
866
|
+
data: doc
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
const effectivePopulate = args.populate ?? (args.depth !== void 0 && args.depth > 0 && args.config && args.collection ? buildDepthPopulate(args.config, args.collection, args.depth) : void 0);
|
|
870
|
+
if (!effectivePopulate || !doc) {
|
|
871
|
+
return doc;
|
|
872
|
+
}
|
|
873
|
+
const [populated] = await populateDocs(
|
|
874
|
+
args.ctx,
|
|
875
|
+
[doc],
|
|
876
|
+
effectivePopulate
|
|
877
|
+
);
|
|
878
|
+
return populated ?? null;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// src/api/search/server.ts
|
|
882
|
+
async function search(args) {
|
|
883
|
+
const { access, action, resource } = resolveAccessCall({
|
|
884
|
+
config: args.config,
|
|
885
|
+
access: args.access,
|
|
886
|
+
defaultAction: CRUD_ACTIONS.read,
|
|
887
|
+
resource: args.collection
|
|
888
|
+
});
|
|
889
|
+
const accessFilter = resolveAccessConstraint({
|
|
890
|
+
access,
|
|
891
|
+
user: args.auth?.user ?? null,
|
|
892
|
+
organization: args.auth?.organization,
|
|
893
|
+
resource,
|
|
894
|
+
action
|
|
895
|
+
});
|
|
896
|
+
const searchQuery = buildQuery2({
|
|
897
|
+
...args,
|
|
898
|
+
accessFilter
|
|
899
|
+
});
|
|
900
|
+
let docs;
|
|
901
|
+
let convexPaginationResult;
|
|
902
|
+
if (args.paginationOpts) {
|
|
903
|
+
convexPaginationResult = await searchQuery.paginate(args.paginationOpts);
|
|
904
|
+
docs = convexPaginationResult.page.filter(
|
|
905
|
+
(d) => hasPermission({
|
|
906
|
+
access,
|
|
907
|
+
user: args.auth?.user ?? {},
|
|
908
|
+
organization: args.auth?.organization,
|
|
909
|
+
resource,
|
|
910
|
+
action,
|
|
911
|
+
data: d
|
|
912
|
+
})
|
|
913
|
+
);
|
|
914
|
+
} else if (args.limit) {
|
|
915
|
+
docs = (await searchQuery.take(args.limit)).filter(
|
|
916
|
+
(d) => hasPermission({
|
|
917
|
+
access,
|
|
918
|
+
user: args.auth?.user ?? {},
|
|
919
|
+
organization: args.auth?.organization,
|
|
920
|
+
resource,
|
|
921
|
+
action,
|
|
922
|
+
data: d
|
|
923
|
+
})
|
|
924
|
+
);
|
|
925
|
+
} else {
|
|
926
|
+
docs = (await searchQuery.collect()).filter(
|
|
927
|
+
(d) => hasPermission({
|
|
928
|
+
access,
|
|
929
|
+
user: args.auth?.user ?? {},
|
|
930
|
+
organization: args.auth?.organization,
|
|
931
|
+
resource,
|
|
932
|
+
action,
|
|
933
|
+
data: d
|
|
934
|
+
})
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
const effectivePopulate = args.populate ?? (args.depth !== void 0 && args.depth > 0 && args.config ? buildDepthPopulate(args.config, args.collection, args.depth) : void 0);
|
|
938
|
+
const finalDocs = !effectivePopulate || Object.keys(effectivePopulate).length === 0 ? docs : await populateDocs(args.ctx, docs, effectivePopulate);
|
|
939
|
+
if (args.paginationOpts) {
|
|
940
|
+
if (args.paginationOpts.totalDocs && !args.paginationOpts.cursor) {
|
|
941
|
+
try {
|
|
942
|
+
const countQuery = buildQuery2(args);
|
|
943
|
+
const allDocs = (await countQuery.collect()).filter(
|
|
944
|
+
(d) => hasPermission({
|
|
945
|
+
access,
|
|
946
|
+
user: args.auth?.user ?? {},
|
|
947
|
+
organization: args.auth?.organization,
|
|
948
|
+
resource,
|
|
949
|
+
action,
|
|
950
|
+
data: d
|
|
951
|
+
})
|
|
952
|
+
);
|
|
953
|
+
if (convexPaginationResult) {
|
|
954
|
+
return {
|
|
955
|
+
...convexPaginationResult,
|
|
956
|
+
page: finalDocs,
|
|
957
|
+
totalDocs: allDocs.length
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
} catch (error) {
|
|
961
|
+
console.warn("Failed to count search results:", error);
|
|
962
|
+
if (convexPaginationResult) {
|
|
963
|
+
return {
|
|
964
|
+
...convexPaginationResult,
|
|
965
|
+
page: finalDocs,
|
|
966
|
+
totalDocs: null
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (convexPaginationResult) {
|
|
972
|
+
return {
|
|
973
|
+
...convexPaginationResult,
|
|
974
|
+
page: finalDocs
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return finalDocs;
|
|
979
|
+
}
|
|
980
|
+
function buildQuery2(args) {
|
|
981
|
+
let q = args.ctx.db.query(args.collection);
|
|
982
|
+
if (args.query) {
|
|
983
|
+
q = q.withSearchIndex(args.searchIndexName, (q2) => q2.search(args.searchField, args.query));
|
|
984
|
+
}
|
|
985
|
+
if (args.accessFilter && args.filter) {
|
|
986
|
+
q = q.filter((fq) => fq.and(args.accessFilter(fq), args.filter(fq)));
|
|
987
|
+
} else if (args.accessFilter) {
|
|
988
|
+
q = q.filter(args.accessFilter);
|
|
989
|
+
} else if (args.filter) {
|
|
990
|
+
q = q.filter(args.filter);
|
|
991
|
+
}
|
|
992
|
+
return q;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// src/api/create/server.ts
|
|
996
|
+
async function create(args) {
|
|
997
|
+
if (args.config.access !== void 0) {
|
|
998
|
+
const { access, action, resource } = resolveAccessCall({
|
|
999
|
+
config: args.config,
|
|
1000
|
+
access: args.access,
|
|
1001
|
+
defaultAction: CRUD_ACTIONS.create,
|
|
1002
|
+
resource: args.collection
|
|
1003
|
+
});
|
|
1004
|
+
hasPermission({
|
|
1005
|
+
access,
|
|
1006
|
+
user: args.auth?.user ?? {},
|
|
1007
|
+
organization: args.auth?.organization,
|
|
1008
|
+
resource,
|
|
1009
|
+
action,
|
|
1010
|
+
data: args.data,
|
|
1011
|
+
throwOnDenied: true
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
const id = await args.ctx.db.insert(
|
|
1015
|
+
args.collection,
|
|
1016
|
+
args.data
|
|
1017
|
+
);
|
|
1018
|
+
return id;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// src/api/update/server.ts
|
|
1022
|
+
async function update(args) {
|
|
1023
|
+
if (args.config.access !== void 0) {
|
|
1024
|
+
const doc = await args.ctx.db.get(args.id);
|
|
1025
|
+
const { access, action, resource } = resolveAccessCall({
|
|
1026
|
+
config: args.config,
|
|
1027
|
+
access: args.access,
|
|
1028
|
+
defaultAction: CRUD_ACTIONS.update,
|
|
1029
|
+
resource: args.collection
|
|
1030
|
+
});
|
|
1031
|
+
hasPermission({
|
|
1032
|
+
throwOnDenied: true,
|
|
1033
|
+
access,
|
|
1034
|
+
user: args.auth?.user ?? {},
|
|
1035
|
+
organization: args.auth?.organization,
|
|
1036
|
+
resource,
|
|
1037
|
+
action,
|
|
1038
|
+
data: doc ?? void 0
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
await args.ctx.db.patch(args.id, args.data);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// src/api/remove/server.ts
|
|
1045
|
+
async function remove(args) {
|
|
1046
|
+
async function removeById(id) {
|
|
1047
|
+
if (args.config.access !== void 0) {
|
|
1048
|
+
const doc = await args.ctx.db.get(id);
|
|
1049
|
+
const { access, action, resource } = resolveAccessCall({
|
|
1050
|
+
config: args.config,
|
|
1051
|
+
access: args.access,
|
|
1052
|
+
defaultAction: CRUD_ACTIONS.delete,
|
|
1053
|
+
resource: args.collection
|
|
1054
|
+
});
|
|
1055
|
+
hasPermission({
|
|
1056
|
+
throwOnDenied: true,
|
|
1057
|
+
access,
|
|
1058
|
+
user: args.auth?.user ?? {},
|
|
1059
|
+
organization: args.auth?.organization,
|
|
1060
|
+
resource,
|
|
1061
|
+
action,
|
|
1062
|
+
data: doc ?? void 0
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
if (args.softDelete) {
|
|
1066
|
+
return await args.ctx.db.patch(args.collection, id, { [args.softDelete]: true });
|
|
1067
|
+
}
|
|
1068
|
+
return await args.ctx.db.delete(args.collection, id);
|
|
1069
|
+
}
|
|
1070
|
+
await Promise.all(args.ids.map((id) => removeById(id)));
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// src/api/globals/get.server.ts
|
|
1074
|
+
function flattenGlobalRow(row) {
|
|
1075
|
+
const { slug, data, _id, _creationTime } = row;
|
|
1076
|
+
return { _id, _creationTime, _slug: slug, ...data ?? {} };
|
|
1077
|
+
}
|
|
1078
|
+
async function getGlobal(args) {
|
|
1079
|
+
const { ctx, slug, populate, depth, config } = args;
|
|
1080
|
+
const row = await ctx.db.query("vex_globals").withIndex("by_slug", (q) => q.eq("slug", slug)).first();
|
|
1081
|
+
if (!row) return null;
|
|
1082
|
+
let flat = flattenGlobalRow(row);
|
|
1083
|
+
if (args.config?.access !== void 0) {
|
|
1084
|
+
const { access, action, resource } = resolveAccessCall({
|
|
1085
|
+
config: args.config,
|
|
1086
|
+
access: args.access,
|
|
1087
|
+
defaultAction: CRUD_ACTIONS.read,
|
|
1088
|
+
resource: args.slug
|
|
1089
|
+
});
|
|
1090
|
+
hasPermission({
|
|
1091
|
+
throwOnDenied: true,
|
|
1092
|
+
user: args.auth?.user ?? {},
|
|
1093
|
+
organization: args.auth?.organization,
|
|
1094
|
+
access,
|
|
1095
|
+
resource,
|
|
1096
|
+
action,
|
|
1097
|
+
data: flat
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
if (depth && depth > 0 && config) {
|
|
1101
|
+
const globalConfig = config.globals.find((g) => g.slug === slug);
|
|
1102
|
+
if (globalConfig) {
|
|
1103
|
+
const depthPopulate = buildDepthPopulate(config, slug, depth);
|
|
1104
|
+
if (depthPopulate && Object.keys(depthPopulate).length > 0) {
|
|
1105
|
+
const [populated] = await populateDocs(ctx, [flat], depthPopulate);
|
|
1106
|
+
flat = populated;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
if (populate && Object.keys(populate).length > 0) {
|
|
1111
|
+
const [populated] = await populateDocs(ctx, [flat], populate);
|
|
1112
|
+
flat = populated;
|
|
1113
|
+
}
|
|
1114
|
+
return flat;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/api/globals/find.server.ts
|
|
1118
|
+
async function findGlobals(args) {
|
|
1119
|
+
const { ctx } = args;
|
|
1120
|
+
let rows = await ctx.db.query("vex_globals").collect();
|
|
1121
|
+
if (args.config.access !== void 0) {
|
|
1122
|
+
rows = rows.filter((r) => {
|
|
1123
|
+
const { access, action, resource } = resolveAccessCall({
|
|
1124
|
+
config: args.config,
|
|
1125
|
+
access: args.access,
|
|
1126
|
+
defaultAction: CRUD_ACTIONS.read,
|
|
1127
|
+
resource: r.slug
|
|
1128
|
+
});
|
|
1129
|
+
return hasPermission({
|
|
1130
|
+
access,
|
|
1131
|
+
user: args.auth?.user ?? {},
|
|
1132
|
+
organization: args.auth?.organization,
|
|
1133
|
+
resource,
|
|
1134
|
+
action,
|
|
1135
|
+
data: r.data
|
|
1136
|
+
});
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
return rows.map((row) => {
|
|
1140
|
+
const { slug, data, _id, _creationTime } = row;
|
|
1141
|
+
return { _id, _creationTime, _slug: slug, ...data ?? {} };
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/api/globals/upsert.server.ts
|
|
1146
|
+
import { ConvexError as ConvexError2 } from "convex/values";
|
|
1147
|
+
|
|
1148
|
+
// src/globals/utils.ts
|
|
1149
|
+
import { z as z13 } from "zod";
|
|
1150
|
+
|
|
1151
|
+
// src/fields/text/inputSchema.ts
|
|
1152
|
+
import { z } from "zod";
|
|
1153
|
+
|
|
1154
|
+
// src/fields/inputSchemas/utils.ts
|
|
1155
|
+
function applyBaseInputSchemaMeta(props) {
|
|
1156
|
+
const { field, inputSchema } = props;
|
|
1157
|
+
const fieldMeta = {
|
|
1158
|
+
fieldType: field.type,
|
|
1159
|
+
label: field.label,
|
|
1160
|
+
description: field.description ?? ""
|
|
1161
|
+
};
|
|
1162
|
+
if (!field.required) {
|
|
1163
|
+
return inputSchema.meta(fieldMeta).optional();
|
|
1164
|
+
}
|
|
1165
|
+
return inputSchema.meta(fieldMeta);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// src/fields/text/inputSchema.ts
|
|
1169
|
+
function textFieldToInputSchema(props) {
|
|
1170
|
+
const { field } = props;
|
|
1171
|
+
const fieldMinError = field.min?.error ?? "This field is too short.";
|
|
1172
|
+
const fieldMaxError = field.max?.error ?? "This field is too long.";
|
|
1173
|
+
let inputSchema = z.string().default(field.defaultValue);
|
|
1174
|
+
if (field.required) {
|
|
1175
|
+
inputSchema = z.string().min(1, "This field is required.").default(field.defaultValue);
|
|
1176
|
+
}
|
|
1177
|
+
if (field.min) {
|
|
1178
|
+
if (field.max) {
|
|
1179
|
+
inputSchema = z.string().min(field.min.value, fieldMinError).max(field.max.value, fieldMaxError).default(field.defaultValue);
|
|
1180
|
+
} else {
|
|
1181
|
+
inputSchema = z.string().min(field.min.value, fieldMinError).default(field.defaultValue);
|
|
1182
|
+
}
|
|
1183
|
+
} else if (field.max) {
|
|
1184
|
+
inputSchema = z.string().max(field.max.value, fieldMaxError).default(field.defaultValue);
|
|
1185
|
+
}
|
|
1186
|
+
return applyBaseInputSchemaMeta({ field, inputSchema });
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// src/fields/number/inputSchema.ts
|
|
1190
|
+
import { z as z2 } from "zod";
|
|
1191
|
+
function numberFieldToInputSchema(props) {
|
|
1192
|
+
const { field } = props;
|
|
1193
|
+
const fieldMinError = field.min?.error ?? "This field is too small.";
|
|
1194
|
+
const fieldMaxError = field.max?.error ?? "This field is too large.";
|
|
1195
|
+
let inputSchema = z2.number().default(field.defaultValue);
|
|
1196
|
+
if (field.min) {
|
|
1197
|
+
let defaultValue = field.defaultValue < field.min.value ? field.min.value : field.defaultValue;
|
|
1198
|
+
if (field.max) {
|
|
1199
|
+
if (field.defaultValue > field.max.value) {
|
|
1200
|
+
defaultValue = field.min.value;
|
|
1201
|
+
}
|
|
1202
|
+
inputSchema = z2.number().min(field.min.value, fieldMinError).max(field.max.value, fieldMaxError).default(defaultValue);
|
|
1203
|
+
} else {
|
|
1204
|
+
inputSchema = z2.number().min(field.min.value, fieldMinError).default(defaultValue);
|
|
1205
|
+
}
|
|
1206
|
+
} else if (field.max) {
|
|
1207
|
+
let defaultValue = field.defaultValue > field.max.value ? field.max.value : field.defaultValue;
|
|
1208
|
+
inputSchema = z2.number().max(field.max.value, fieldMaxError).default(defaultValue);
|
|
1209
|
+
}
|
|
1210
|
+
return applyBaseInputSchemaMeta({ field, inputSchema });
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// src/fields/checkbox/inputSchema.ts
|
|
1214
|
+
import { z as z3 } from "zod";
|
|
1215
|
+
function checkboxFieldToInputSchema(props) {
|
|
1216
|
+
const { field } = props;
|
|
1217
|
+
const inputSchema = z3.boolean().default(field.defaultValue);
|
|
1218
|
+
return applyBaseInputSchemaMeta({ field, inputSchema });
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// src/fields/date/inputSchema.ts
|
|
1222
|
+
import { z as z4 } from "zod";
|
|
1223
|
+
function dateFieldToInputSchema(props) {
|
|
1224
|
+
const { field } = props;
|
|
1225
|
+
let inputSchema = z4.number();
|
|
1226
|
+
if (field.required) {
|
|
1227
|
+
inputSchema = z4.number().default((/* @__PURE__ */ new Date()).getTime());
|
|
1228
|
+
}
|
|
1229
|
+
return applyBaseInputSchemaMeta({ field, inputSchema });
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// src/fields/select/inputSchema.ts
|
|
1233
|
+
import { z as z5 } from "zod";
|
|
1234
|
+
function selectFieldToInputSchema(props) {
|
|
1235
|
+
const { field } = props;
|
|
1236
|
+
const optionValues = field.options.map((o) => o.value);
|
|
1237
|
+
const itemSchema = optionValues.length > 0 ? z5.enum(optionValues) : z5.string();
|
|
1238
|
+
let inputSchema = z5.array(itemSchema).default(field.defaultValue);
|
|
1239
|
+
if (!field.hasMany) {
|
|
1240
|
+
inputSchema = z5.array(itemSchema).max(1, "Only one value may be selected.").default(field.defaultValue);
|
|
1241
|
+
}
|
|
1242
|
+
return applyBaseInputSchemaMeta({ field, inputSchema });
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/fields/url/inputSchema.ts
|
|
1246
|
+
import { z as z6 } from "zod";
|
|
1247
|
+
function urlFieldToInputSchema(props) {
|
|
1248
|
+
const { field } = props;
|
|
1249
|
+
let inputSchema = z6.url();
|
|
1250
|
+
if (field.required) {
|
|
1251
|
+
inputSchema = z6.url().min(1, "This field is required.");
|
|
1252
|
+
if (field.defaultValue) {
|
|
1253
|
+
inputSchema = z6.url().min(1, "This field is required.").default(field.defaultValue);
|
|
1254
|
+
}
|
|
1255
|
+
} else if (field.defaultValue !== void 0) {
|
|
1256
|
+
inputSchema = z6.union([z6.url(), z6.literal("")]).default(field.defaultValue);
|
|
1257
|
+
}
|
|
1258
|
+
return applyBaseInputSchemaMeta({ field, inputSchema });
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// src/fields/color/formats.ts
|
|
1262
|
+
var COLOR_FORMATS = {
|
|
1263
|
+
hex: {
|
|
1264
|
+
format: "hex",
|
|
1265
|
+
/** `#E8622A`, `#E8622A80`. Shorthand is rejected — the picker never emits it. */
|
|
1266
|
+
pattern: /^#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?$/
|
|
1267
|
+
},
|
|
1268
|
+
rgb: {
|
|
1269
|
+
format: "rgb",
|
|
1270
|
+
/** `rgb(232, 98, 42)`, `rgba(232, 98, 42, 0.5)`. */
|
|
1271
|
+
pattern: /^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(?:,\s*(?:0|1|0?\.\d+)\s*)?\)$/
|
|
1272
|
+
},
|
|
1273
|
+
hsl: {
|
|
1274
|
+
format: "hsl",
|
|
1275
|
+
/** `hsl(17.7, 81%, 54%)`, `hsla(17.7, 81%, 54%, 0.5)`. */
|
|
1276
|
+
pattern: /^hsla?\(\s*-?\d+(?:\.\d+)?\s*,\s*\d+(?:\.\d+)?%\s*,\s*\d+(?:\.\d+)?%\s*(?:,\s*(?:0|1|0?\.\d+)\s*)?\)$/
|
|
1277
|
+
},
|
|
1278
|
+
oklch: {
|
|
1279
|
+
format: "oklch",
|
|
1280
|
+
/** `oklch(65.7% 0.179 40.9)`, `oklch(65.7% 0.179 40.9 / 0.5)`. */
|
|
1281
|
+
pattern: /^oklch\(\s*\d+(?:\.\d+)?%\s+\d+(?:\.\d+)?\s+-?\d+(?:\.\d+)?\s*(?:\/\s*(?:0|1|0?\.\d+)\s*)?\)$/
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
var COLOR_FORMAT_PATTERNS = Object.values(COLOR_FORMATS).map((f) => f.pattern);
|
|
1285
|
+
var CSS_VAR_REFERENCE = /^var\(--[A-Za-z0-9_-]+\)$/;
|
|
1286
|
+
|
|
1287
|
+
// src/fields/color/inputSchema.ts
|
|
1288
|
+
import { z as z7 } from "zod";
|
|
1289
|
+
var FORMAT_EXAMPLES = {
|
|
1290
|
+
hex: "#E8622A",
|
|
1291
|
+
rgb: "rgb(232, 98, 42)",
|
|
1292
|
+
hsl: "hsl(17.7, 81%, 54%)",
|
|
1293
|
+
oklch: "oklch(65.7% 0.179 40.9)"
|
|
1294
|
+
};
|
|
1295
|
+
function colorPattern(props) {
|
|
1296
|
+
const sources = COLOR_FORMAT_PATTERNS.map((pattern) => pattern.source);
|
|
1297
|
+
if (props.themeColors) sources.push(CSS_VAR_REFERENCE.source);
|
|
1298
|
+
return new RegExp(sources.join("|"));
|
|
1299
|
+
}
|
|
1300
|
+
function colorMessage(props) {
|
|
1301
|
+
const example = FORMAT_EXAMPLES[props.field.format];
|
|
1302
|
+
const base = `Enter a colour, e.g. ${example}.`;
|
|
1303
|
+
if (!props.field.themeColors) return base;
|
|
1304
|
+
return `${base} A theme token such as var(--primary) is also accepted.`;
|
|
1305
|
+
}
|
|
1306
|
+
function colorFieldToInputSchema(props) {
|
|
1307
|
+
const { field } = props;
|
|
1308
|
+
const pattern = colorPattern({ themeColors: field.themeColors });
|
|
1309
|
+
const message = colorMessage({ field });
|
|
1310
|
+
let inputSchema = z7.string().regex(pattern, message);
|
|
1311
|
+
if (field.required) {
|
|
1312
|
+
inputSchema = z7.string().min(1, "This field is required.").regex(pattern, message);
|
|
1313
|
+
if (field.defaultValue) {
|
|
1314
|
+
inputSchema = inputSchema.default(field.defaultValue);
|
|
1315
|
+
}
|
|
1316
|
+
} else if (field.defaultValue !== void 0) {
|
|
1317
|
+
inputSchema = z7.union([z7.string().regex(pattern, message), z7.literal("")]).default(field.defaultValue);
|
|
1318
|
+
}
|
|
1319
|
+
return applyBaseInputSchemaMeta({ field, inputSchema });
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/fields/relationship/inputSchema.ts
|
|
1323
|
+
import { z as z8 } from "zod";
|
|
1324
|
+
function relationshipFieldToInputSchema(props) {
|
|
1325
|
+
const { field } = props;
|
|
1326
|
+
const inputSchema = z8.array(z8.string()).default(ADMIN_FIELDS.relationship.defaultValue);
|
|
1327
|
+
return applyBaseInputSchemaMeta({ field, inputSchema });
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/fields/array/inputSchema.ts
|
|
1331
|
+
import { z as z12 } from "zod";
|
|
1332
|
+
|
|
1333
|
+
// src/fields/group/inputSchema.ts
|
|
1334
|
+
import { z as z9 } from "zod";
|
|
1335
|
+
function groupFieldToInputSchema(props) {
|
|
1336
|
+
const { field } = props;
|
|
1337
|
+
const subSchemas = Object.fromEntries(
|
|
1338
|
+
Object.entries(field.fields).map(([key, subField]) => [
|
|
1339
|
+
key,
|
|
1340
|
+
adminFieldToInputSchema({ field: subField })
|
|
1341
|
+
])
|
|
1342
|
+
);
|
|
1343
|
+
const schema = z9.object(subSchemas).default(field.defaultValue ?? {});
|
|
1344
|
+
return applyBaseInputSchemaMeta({ field, inputSchema: schema });
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/fields/blocks/inputSchema.ts
|
|
1348
|
+
import { z as z10 } from "zod";
|
|
1349
|
+
function blocksFieldToInputSchema(props) {
|
|
1350
|
+
const { field } = props;
|
|
1351
|
+
const blockSchemas = field.blocks.map((block) => {
|
|
1352
|
+
const userSubSchemas = Object.fromEntries(
|
|
1353
|
+
Object.entries(block.fields).map(([key, subField]) => [
|
|
1354
|
+
key,
|
|
1355
|
+
adminFieldToInputSchema({ field: subField })
|
|
1356
|
+
])
|
|
1357
|
+
);
|
|
1358
|
+
return z10.object({
|
|
1359
|
+
blockType: z10.literal(block.blockType),
|
|
1360
|
+
blockName: z10.string().optional(),
|
|
1361
|
+
id: z10.string(),
|
|
1362
|
+
...userSubSchemas
|
|
1363
|
+
});
|
|
1364
|
+
});
|
|
1365
|
+
const itemSchema = blockSchemas.length <= 1 ? blockSchemas[0] ?? z10.object({ blockType: z10.string(), id: z10.string() }) : (
|
|
1366
|
+
// @ts-expect-error mismatched zod types, works in practice
|
|
1367
|
+
z10.discriminatedUnion("blockType", blockSchemas)
|
|
1368
|
+
);
|
|
1369
|
+
let schema = z10.array(itemSchema);
|
|
1370
|
+
if (field.min) {
|
|
1371
|
+
schema = schema.min(field.min, `At least ${field.min} ${field.labels.plural} required.`);
|
|
1372
|
+
}
|
|
1373
|
+
if (field.max) {
|
|
1374
|
+
schema = schema.max(field.max, `No more than ${field.max} ${field.labels.plural} allowed.`);
|
|
1375
|
+
}
|
|
1376
|
+
schema = schema.default(field.defaultValue ?? []);
|
|
1377
|
+
return applyBaseInputSchemaMeta({ field, inputSchema: schema });
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/fields/upload/inputSchema.ts
|
|
1381
|
+
import { z as z11 } from "zod";
|
|
1382
|
+
function uploadFieldToInputSchema(props) {
|
|
1383
|
+
const inputSchema = z11.array(z11.string());
|
|
1384
|
+
return applyBaseInputSchemaMeta({ field: props.field, inputSchema });
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// src/fields/inputSchemas/index.ts
|
|
1388
|
+
function adminFieldToInputSchema(props) {
|
|
1389
|
+
const fieldType = props.field.type;
|
|
1390
|
+
switch (props.field.type) {
|
|
1391
|
+
case ADMIN_FIELDS.text.type:
|
|
1392
|
+
return textFieldToInputSchema({ field: props.field });
|
|
1393
|
+
case ADMIN_FIELDS.number.type:
|
|
1394
|
+
return numberFieldToInputSchema({ field: props.field });
|
|
1395
|
+
case ADMIN_FIELDS.checkbox.type:
|
|
1396
|
+
return checkboxFieldToInputSchema({ field: props.field });
|
|
1397
|
+
case ADMIN_FIELDS.date.type:
|
|
1398
|
+
return dateFieldToInputSchema({ field: props.field });
|
|
1399
|
+
case ADMIN_FIELDS.select.type:
|
|
1400
|
+
return selectFieldToInputSchema({ field: props.field });
|
|
1401
|
+
case ADMIN_FIELDS.url.type:
|
|
1402
|
+
return urlFieldToInputSchema({ field: props.field });
|
|
1403
|
+
case ADMIN_FIELDS.color.type:
|
|
1404
|
+
return colorFieldToInputSchema({ field: props.field });
|
|
1405
|
+
case ADMIN_FIELDS.relationship.type:
|
|
1406
|
+
return relationshipFieldToInputSchema({ field: props.field });
|
|
1407
|
+
case ADMIN_FIELDS.array.type:
|
|
1408
|
+
return arrayFieldToInputSchema({ field: props.field });
|
|
1409
|
+
case ADMIN_FIELDS.group.type:
|
|
1410
|
+
return groupFieldToInputSchema({ field: props.field });
|
|
1411
|
+
case ADMIN_FIELDS.blocks.type:
|
|
1412
|
+
return blocksFieldToInputSchema({ field: props.field });
|
|
1413
|
+
case ADMIN_FIELDS.upload.type:
|
|
1414
|
+
return uploadFieldToInputSchema({ field: props.field });
|
|
1415
|
+
default: {
|
|
1416
|
+
const unhandled = props.field;
|
|
1417
|
+
throw new Error(
|
|
1418
|
+
`unrecognized field type: ${fieldType} \u2014 ${JSON.stringify(unhandled)}`
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// src/fields/array/inputSchema.ts
|
|
1425
|
+
function arrayFieldToInputSchema(props) {
|
|
1426
|
+
const { field } = props;
|
|
1427
|
+
const fieldMinError = field.min?.error ?? "This field is too short.";
|
|
1428
|
+
const fieldMaxError = field.max?.error ?? "This field is too long.";
|
|
1429
|
+
const itemsInputSchema = adminFieldToInputSchema({ field: field.items });
|
|
1430
|
+
let inputSchema = z12.array(itemsInputSchema).default(field.defaultValue ?? []);
|
|
1431
|
+
if (field.required) {
|
|
1432
|
+
inputSchema = z12.array(itemsInputSchema).superRefine((val, ctx) => {
|
|
1433
|
+
if (val === void 0 || val === null) {
|
|
1434
|
+
ctx.addIssue({
|
|
1435
|
+
code: "custom",
|
|
1436
|
+
message: "This field is required."
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
}).default(field.defaultValue ?? []);
|
|
1440
|
+
}
|
|
1441
|
+
if (field.min) {
|
|
1442
|
+
if (field.max) {
|
|
1443
|
+
inputSchema = z12.array(itemsInputSchema).min(field.min.value, fieldMinError).max(field.max.value, fieldMaxError).default(field.defaultValue ?? []);
|
|
1444
|
+
} else {
|
|
1445
|
+
inputSchema = z12.array(itemsInputSchema).min(field.min.value, fieldMinError).default(field.defaultValue ?? []);
|
|
1446
|
+
}
|
|
1447
|
+
} else if (field.max) {
|
|
1448
|
+
inputSchema = z12.array(itemsInputSchema).max(field.max.value, fieldMaxError).default(field.defaultValue ?? []);
|
|
1449
|
+
}
|
|
1450
|
+
let finalSchema = inputSchema;
|
|
1451
|
+
if (!field.required && field.defaultValue !== void 0) {
|
|
1452
|
+
finalSchema = inputSchema.default(field.defaultValue);
|
|
1453
|
+
}
|
|
1454
|
+
finalSchema = applyBaseInputSchemaMeta({
|
|
1455
|
+
field,
|
|
1456
|
+
inputSchema: finalSchema
|
|
1457
|
+
});
|
|
1458
|
+
return finalSchema;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/globals/utils.ts
|
|
1462
|
+
function getGlobalDefaultValues(props) {
|
|
1463
|
+
const res = {};
|
|
1464
|
+
for (const [fieldKey, fieldDef] of Object.entries(props.global.fields)) {
|
|
1465
|
+
if (fieldDef.admin.hidden) continue;
|
|
1466
|
+
if (props.document && Boolean(props.document[fieldKey])) {
|
|
1467
|
+
res[fieldKey] = props.document[fieldKey];
|
|
1468
|
+
} else {
|
|
1469
|
+
res[fieldKey] = fieldDef.defaultValue;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
return res;
|
|
1473
|
+
}
|
|
1474
|
+
function getGlobalInputSchema(props) {
|
|
1475
|
+
const res = {};
|
|
1476
|
+
for (const [fieldKey, fieldDef] of Object.entries(props.global.fields)) {
|
|
1477
|
+
if (fieldDef.admin.hidden) continue;
|
|
1478
|
+
res[fieldKey] = adminFieldToInputSchema({ field: fieldDef });
|
|
1479
|
+
}
|
|
1480
|
+
return z13.object({ ...res });
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// src/api/globals/upsert.server.ts
|
|
1484
|
+
var STRIPPED_KEYS = /* @__PURE__ */ new Set(["_id", "_creationTime", "_slug"]);
|
|
1485
|
+
async function upsertGlobal(args) {
|
|
1486
|
+
const { ctx, slug, data } = args;
|
|
1487
|
+
const globalConfig = args.config.globals.find((g) => g.slug === args.slug);
|
|
1488
|
+
if (!globalConfig) {
|
|
1489
|
+
throw new ConvexError2(`No global registered with slug "${args.slug}"`);
|
|
1490
|
+
}
|
|
1491
|
+
if (args.config.access !== void 0) {
|
|
1492
|
+
const { access, action, resource } = resolveAccessCall({
|
|
1493
|
+
config: args.config,
|
|
1494
|
+
access: args.access,
|
|
1495
|
+
defaultAction: CRUD_ACTIONS.read,
|
|
1496
|
+
resource: args.slug
|
|
1497
|
+
});
|
|
1498
|
+
hasPermission({
|
|
1499
|
+
throwOnDenied: true,
|
|
1500
|
+
access,
|
|
1501
|
+
user: args.auth?.user ?? {},
|
|
1502
|
+
organization: args.auth?.organization,
|
|
1503
|
+
resource,
|
|
1504
|
+
action
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
const userFields = {};
|
|
1508
|
+
for (const [k, v2] of Object.entries(data)) {
|
|
1509
|
+
if (!STRIPPED_KEYS.has(k)) userFields[k] = v2;
|
|
1510
|
+
}
|
|
1511
|
+
const schema = getGlobalInputSchema({ global: globalConfig });
|
|
1512
|
+
const result = schema.safeParse(userFields);
|
|
1513
|
+
if (!result.success) {
|
|
1514
|
+
throw new ConvexError2({
|
|
1515
|
+
message: "Global validation failed",
|
|
1516
|
+
errors: result.error.message
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
const existingGlobal = await ctx.db.query("vex_globals").withIndex("by_slug", (q) => q.eq("slug", slug)).first();
|
|
1520
|
+
if (existingGlobal) {
|
|
1521
|
+
await ctx.db.patch(existingGlobal._id, { data: result.data });
|
|
1522
|
+
return existingGlobal._id;
|
|
1523
|
+
}
|
|
1524
|
+
const id = await ctx.db.insert("vex_globals", {
|
|
1525
|
+
slug,
|
|
1526
|
+
data: result.data
|
|
1527
|
+
});
|
|
1528
|
+
return id;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// src/api/server.ts
|
|
1532
|
+
function collectionsApi({
|
|
1533
|
+
config,
|
|
1534
|
+
query,
|
|
1535
|
+
mutation,
|
|
1536
|
+
getAuth
|
|
1537
|
+
}) {
|
|
1538
|
+
return {
|
|
1539
|
+
find: query({
|
|
1540
|
+
args: {
|
|
1541
|
+
collection: v.string(),
|
|
1542
|
+
populate: v.optional(v.any()),
|
|
1543
|
+
depth: v.optional(v.number()),
|
|
1544
|
+
limit: v.optional(v.number()),
|
|
1545
|
+
paginationOpts: v.optional(
|
|
1546
|
+
paginationOptsValidator.extend({ totalDocs: v.optional(v.boolean()) })
|
|
1547
|
+
)
|
|
1548
|
+
},
|
|
1549
|
+
handler: async (ctx, args) => {
|
|
1550
|
+
const auth = await resolveGetAuth({ ctx, config, getAuth });
|
|
1551
|
+
return await find({
|
|
1552
|
+
ctx,
|
|
1553
|
+
auth,
|
|
1554
|
+
collection: args.collection,
|
|
1555
|
+
populate: args.populate,
|
|
1556
|
+
depth: args.depth,
|
|
1557
|
+
config,
|
|
1558
|
+
limit: args.limit,
|
|
1559
|
+
paginationOpts: args.paginationOpts
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
}),
|
|
1563
|
+
get: query({
|
|
1564
|
+
args: {
|
|
1565
|
+
collection: v.string(),
|
|
1566
|
+
id: v.string(),
|
|
1567
|
+
populate: v.optional(v.any()),
|
|
1568
|
+
depth: v.optional(v.number())
|
|
1569
|
+
},
|
|
1570
|
+
handler: async (ctx, args) => {
|
|
1571
|
+
const auth = await resolveGetAuth({ ctx, config, getAuth });
|
|
1572
|
+
return await get({
|
|
1573
|
+
auth,
|
|
1574
|
+
ctx,
|
|
1575
|
+
collection: args.collection,
|
|
1576
|
+
id: args.id,
|
|
1577
|
+
populate: args.populate,
|
|
1578
|
+
depth: args.depth,
|
|
1579
|
+
config
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
}),
|
|
1583
|
+
search: query({
|
|
1584
|
+
args: {
|
|
1585
|
+
collection: v.string(),
|
|
1586
|
+
searchIndexName: v.string(),
|
|
1587
|
+
searchField: v.string(),
|
|
1588
|
+
query: v.string(),
|
|
1589
|
+
limit: v.optional(v.number()),
|
|
1590
|
+
populate: v.optional(v.any()),
|
|
1591
|
+
depth: v.optional(v.number()),
|
|
1592
|
+
paginationOpts: v.optional(
|
|
1593
|
+
paginationOptsValidator.extend({ totalDocs: v.optional(v.boolean()) })
|
|
1594
|
+
)
|
|
1595
|
+
},
|
|
1596
|
+
handler: async (ctx, args) => {
|
|
1597
|
+
const auth = await resolveGetAuth({ ctx, config, getAuth });
|
|
1598
|
+
return search({
|
|
1599
|
+
auth,
|
|
1600
|
+
ctx,
|
|
1601
|
+
collection: args.collection,
|
|
1602
|
+
query: args.query,
|
|
1603
|
+
searchIndexName: args.searchIndexName,
|
|
1604
|
+
searchField: args.searchField,
|
|
1605
|
+
limit: args.limit,
|
|
1606
|
+
populate: args.populate,
|
|
1607
|
+
depth: args.depth,
|
|
1608
|
+
paginationOpts: args.paginationOpts,
|
|
1609
|
+
config
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
}),
|
|
1613
|
+
// MUTATIONS
|
|
1614
|
+
create: mutation({
|
|
1615
|
+
args: {
|
|
1616
|
+
collection: v.string(),
|
|
1617
|
+
data: v.any()
|
|
1618
|
+
},
|
|
1619
|
+
returns: v.string(),
|
|
1620
|
+
handler: async (ctx, args) => {
|
|
1621
|
+
const auth = await resolveGetAuth({ ctx, config, getAuth });
|
|
1622
|
+
return await create({
|
|
1623
|
+
auth,
|
|
1624
|
+
ctx,
|
|
1625
|
+
config,
|
|
1626
|
+
collection: args.collection,
|
|
1627
|
+
data: args.data
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
}),
|
|
1631
|
+
update: mutation({
|
|
1632
|
+
args: {
|
|
1633
|
+
id: v.string(),
|
|
1634
|
+
collection: v.string(),
|
|
1635
|
+
data: v.any()
|
|
1636
|
+
},
|
|
1637
|
+
handler: async (ctx, args) => {
|
|
1638
|
+
const auth = await resolveGetAuth({ ctx, config, getAuth });
|
|
1639
|
+
return update({
|
|
1640
|
+
auth,
|
|
1641
|
+
ctx,
|
|
1642
|
+
config,
|
|
1643
|
+
collection: args.collection,
|
|
1644
|
+
id: args.id,
|
|
1645
|
+
data: args.data
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
}),
|
|
1649
|
+
remove: mutation({
|
|
1650
|
+
args: {
|
|
1651
|
+
collection: v.string(),
|
|
1652
|
+
ids: v.array(v.string()),
|
|
1653
|
+
softDelete: v.optional(v.string())
|
|
1654
|
+
},
|
|
1655
|
+
handler: async (ctx, args) => {
|
|
1656
|
+
const auth = await resolveGetAuth({ ctx, config, getAuth });
|
|
1657
|
+
return remove({
|
|
1658
|
+
auth,
|
|
1659
|
+
collection: args.collection,
|
|
1660
|
+
ctx,
|
|
1661
|
+
config,
|
|
1662
|
+
ids: args.ids,
|
|
1663
|
+
softDelete: args.softDelete
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
})
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
function globalsApi({
|
|
1670
|
+
config,
|
|
1671
|
+
query,
|
|
1672
|
+
mutation,
|
|
1673
|
+
getAuth
|
|
1674
|
+
}) {
|
|
1675
|
+
return {
|
|
1676
|
+
get: query({
|
|
1677
|
+
args: {
|
|
1678
|
+
slug: v.string(),
|
|
1679
|
+
populate: v.optional(v.any())
|
|
1680
|
+
},
|
|
1681
|
+
handler: async (ctx, args) => {
|
|
1682
|
+
const auth = await resolveGetAuth({ ctx, config, getAuth });
|
|
1683
|
+
return await getGlobal({
|
|
1684
|
+
auth,
|
|
1685
|
+
ctx,
|
|
1686
|
+
slug: args.slug,
|
|
1687
|
+
populate: args.populate,
|
|
1688
|
+
config
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
}),
|
|
1692
|
+
find: query({
|
|
1693
|
+
args: {},
|
|
1694
|
+
handler: async (ctx) => {
|
|
1695
|
+
const auth = await resolveGetAuth({ ctx, config, getAuth });
|
|
1696
|
+
return await findGlobals({ auth, ctx, config });
|
|
1697
|
+
}
|
|
1698
|
+
}),
|
|
1699
|
+
upsert: mutation({
|
|
1700
|
+
args: {
|
|
1701
|
+
slug: v.string(),
|
|
1702
|
+
data: v.any()
|
|
1703
|
+
},
|
|
1704
|
+
returns: v.string(),
|
|
1705
|
+
handler: async (ctx, args) => {
|
|
1706
|
+
const auth = await resolveGetAuth({ ctx, config, getAuth });
|
|
1707
|
+
return await upsertGlobal({
|
|
1708
|
+
auth,
|
|
1709
|
+
ctx,
|
|
1710
|
+
config,
|
|
1711
|
+
slug: args.slug,
|
|
1712
|
+
data: args.data
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
})
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
async function resolveGetAuth({
|
|
1719
|
+
config,
|
|
1720
|
+
ctx,
|
|
1721
|
+
getAuth
|
|
1722
|
+
}) {
|
|
1723
|
+
let auth = void 0;
|
|
1724
|
+
if (config.access !== void 0) {
|
|
1725
|
+
if (!getAuth) {
|
|
1726
|
+
throw new VexAccessConfigError(
|
|
1727
|
+
"getAuth not configured in collectionsApi. configure @ /src/convex/vex.ts"
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
auth = await getAuth(ctx);
|
|
1731
|
+
}
|
|
1732
|
+
return auth;
|
|
1733
|
+
}
|
|
1734
|
+
function vexServerApi(options) {
|
|
1735
|
+
const { config, getAuth } = options;
|
|
1736
|
+
async function inject(ctx, bypassAccess) {
|
|
1737
|
+
const authConfig = bypassAccess === true ? { ...config, access: void 0 } : config;
|
|
1738
|
+
return { config, auth: await resolveGetAuth({ ctx, config: authConfig, getAuth }) };
|
|
1739
|
+
}
|
|
1740
|
+
return {
|
|
1741
|
+
get: async (args) => {
|
|
1742
|
+
const { access, ...rest } = args;
|
|
1743
|
+
return await get({
|
|
1744
|
+
access,
|
|
1745
|
+
...rest,
|
|
1746
|
+
...await inject(rest.ctx, access?.bypass)
|
|
1747
|
+
});
|
|
1748
|
+
},
|
|
1749
|
+
// Cast to the overloaded interface: an implementation signature cannot be
|
|
1750
|
+
// structurally assignable to two call signatures at once.
|
|
1751
|
+
find: (async (args) => {
|
|
1752
|
+
const { access, ...rest } = args;
|
|
1753
|
+
return await find({
|
|
1754
|
+
access,
|
|
1755
|
+
...rest,
|
|
1756
|
+
...await inject(rest.ctx, access?.bypass)
|
|
1757
|
+
});
|
|
1758
|
+
}),
|
|
1759
|
+
search: (async (args) => {
|
|
1760
|
+
const { access, ...rest } = args;
|
|
1761
|
+
return await search({
|
|
1762
|
+
access,
|
|
1763
|
+
...rest,
|
|
1764
|
+
...await inject(rest.ctx, access?.bypass)
|
|
1765
|
+
});
|
|
1766
|
+
}),
|
|
1767
|
+
create: async (args) => {
|
|
1768
|
+
const { access, ...rest } = args;
|
|
1769
|
+
return await create({
|
|
1770
|
+
access,
|
|
1771
|
+
...rest,
|
|
1772
|
+
...await inject(rest.ctx, access?.bypass)
|
|
1773
|
+
});
|
|
1774
|
+
},
|
|
1775
|
+
update: async (args) => {
|
|
1776
|
+
const { access, ...rest } = args;
|
|
1777
|
+
return await update({
|
|
1778
|
+
access,
|
|
1779
|
+
...rest,
|
|
1780
|
+
...await inject(rest.ctx, access?.bypass)
|
|
1781
|
+
});
|
|
1782
|
+
},
|
|
1783
|
+
remove: async (args) => {
|
|
1784
|
+
const { access, ...rest } = args;
|
|
1785
|
+
return await remove({
|
|
1786
|
+
access,
|
|
1787
|
+
...rest,
|
|
1788
|
+
...await inject(rest.ctx, access?.bypass)
|
|
1789
|
+
});
|
|
1790
|
+
},
|
|
1791
|
+
globals: {
|
|
1792
|
+
get: async (args) => {
|
|
1793
|
+
const { access, ...rest } = args;
|
|
1794
|
+
return await getGlobal({
|
|
1795
|
+
access,
|
|
1796
|
+
...rest,
|
|
1797
|
+
...await inject(rest.ctx, access?.bypass)
|
|
1798
|
+
});
|
|
1799
|
+
},
|
|
1800
|
+
find: async (args) => {
|
|
1801
|
+
const { access, ...rest } = args;
|
|
1802
|
+
return await findGlobals({
|
|
1803
|
+
access,
|
|
1804
|
+
...rest,
|
|
1805
|
+
...await inject(rest.ctx, access?.bypass)
|
|
1806
|
+
});
|
|
1807
|
+
},
|
|
1808
|
+
upsert: async (args) => {
|
|
1809
|
+
const { access, ...rest } = args;
|
|
1810
|
+
return await upsertGlobal({
|
|
1811
|
+
access,
|
|
1812
|
+
...rest,
|
|
1813
|
+
...await inject(rest.ctx, access?.bypass)
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
export {
|
|
1821
|
+
ADMIN_FIELDS,
|
|
1822
|
+
textFieldToInputSchema,
|
|
1823
|
+
numberFieldToInputSchema,
|
|
1824
|
+
checkboxFieldToInputSchema,
|
|
1825
|
+
dateFieldToInputSchema,
|
|
1826
|
+
selectFieldToInputSchema,
|
|
1827
|
+
urlFieldToInputSchema,
|
|
1828
|
+
COLOR_FORMATS,
|
|
1829
|
+
COLOR_FORMAT_PATTERNS,
|
|
1830
|
+
CSS_VAR_REFERENCE,
|
|
1831
|
+
colorFieldToInputSchema,
|
|
1832
|
+
relationshipFieldToInputSchema,
|
|
1833
|
+
groupFieldToInputSchema,
|
|
1834
|
+
blocksFieldToInputSchema,
|
|
1835
|
+
uploadFieldToInputSchema,
|
|
1836
|
+
adminFieldToInputSchema,
|
|
1837
|
+
arrayFieldToInputSchema,
|
|
1838
|
+
getGlobalDefaultValues,
|
|
1839
|
+
getGlobalInputSchema,
|
|
1840
|
+
dataType,
|
|
1841
|
+
VexAccessError,
|
|
1842
|
+
VexAccessConfigError,
|
|
1843
|
+
createFilterConstraintBuilder,
|
|
1844
|
+
createIndexConstraintBuilder,
|
|
1845
|
+
readIndexConstraints,
|
|
1846
|
+
createAccessQueryBuilder,
|
|
1847
|
+
readAccessCondition,
|
|
1848
|
+
CRUD_ACTIONS,
|
|
1849
|
+
DRAFT_ACTIONS,
|
|
1850
|
+
QUERY_ACTIONS,
|
|
1851
|
+
PERMISSION_MODES,
|
|
1852
|
+
PERMISSION_SCOPES,
|
|
1853
|
+
WILDCARD_KEY,
|
|
1854
|
+
ADMIN_CUSTOM_SUBJECTS,
|
|
1855
|
+
hasPermission,
|
|
1856
|
+
resolveActionCheck,
|
|
1857
|
+
resolveAccessIndex,
|
|
1858
|
+
resolveAccessConstraint,
|
|
1859
|
+
pickQueryIndex,
|
|
1860
|
+
resolveAccessCall,
|
|
1861
|
+
resolveCollectionSlug,
|
|
1862
|
+
buildDepthPopulate,
|
|
1863
|
+
find,
|
|
1864
|
+
get,
|
|
1865
|
+
search,
|
|
1866
|
+
create,
|
|
1867
|
+
update,
|
|
1868
|
+
remove,
|
|
1869
|
+
getGlobal,
|
|
1870
|
+
findGlobals,
|
|
1871
|
+
upsertGlobal,
|
|
1872
|
+
collectionsApi,
|
|
1873
|
+
globalsApi,
|
|
1874
|
+
resolveGetAuth,
|
|
1875
|
+
vexServerApi
|
|
1876
|
+
};
|
|
1877
|
+
//# sourceMappingURL=chunk-2DU7JDEX.js.map
|