@rebasepro/common 0.23.0 → 0.24.1-canary.g56af4a4
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/collections/CollectionRegistry.d.ts +72 -3
- package/dist/collections/CollectionRegistry.es.js +404 -0
- package/dist/collections/CollectionRegistry.es.js.map +1 -0
- package/dist/collections/auth-secrets.es.js +47 -0
- package/dist/collections/auth-secrets.es.js.map +1 -0
- package/dist/collections/default-collections.es.js +120 -0
- package/dist/collections/default-collections.es.js.map +1 -0
- package/dist/collections/engine-support.es.js +35 -0
- package/dist/collections/engine-support.es.js.map +1 -0
- package/dist/collections/field-access.d.ts +13 -0
- package/dist/collections/field-access.es.js +142 -0
- package/dist/collections/field-access.es.js.map +1 -0
- package/dist/collections/index.d.ts +1 -0
- package/dist/collections/soft-delete.es.js +34 -0
- package/dist/collections/soft-delete.es.js.map +1 -0
- package/dist/collections/unknown-field.d.ts +65 -0
- package/dist/collections/unknown-field.es.js +135 -0
- package/dist/collections/unknown-field.es.js.map +1 -0
- package/dist/data/buildRebaseData.d.ts +27 -2
- package/dist/data/buildRebaseData.es.js +1026 -0
- package/dist/data/buildRebaseData.es.js.map +1 -0
- package/dist/data/buildRoutedRebaseData.d.ts +6 -1
- package/dist/data/buildRoutedRebaseData.es.js +47 -0
- package/dist/data/buildRoutedRebaseData.es.js.map +1 -0
- package/dist/data/collection-accessor.d.ts +54 -0
- package/dist/data/collection-accessor.es.js +101 -0
- package/dist/data/collection-accessor.es.js.map +1 -0
- package/dist/data/cursor.d.ts +1 -1
- package/dist/data/cursor.es.js +166 -0
- package/dist/data/cursor.es.js.map +1 -0
- package/dist/data/filter-conditions.es.js +24 -0
- package/dist/data/filter-conditions.es.js.map +1 -0
- package/dist/data/filter-dialect.d.ts +3 -3
- package/dist/data/filter-dialect.es.js +786 -0
- package/dist/data/filter-dialect.es.js.map +1 -0
- package/dist/data/include-spec.d.ts +2 -0
- package/dist/data/include-spec.es.js +273 -0
- package/dist/data/include-spec.es.js.map +1 -0
- package/dist/data/paginate.d.ts +8 -1
- package/dist/data/paginate.es.js +181 -0
- package/dist/data/paginate.es.js.map +1 -0
- package/dist/data/query_builder.d.ts +52 -4
- package/dist/data/query_builder.es.js +151 -0
- package/dist/data/query_builder.es.js.map +1 -0
- package/dist/data/resolveDataSource.es.js +87 -0
- package/dist/data/resolveDataSource.es.js.map +1 -0
- package/dist/data/row-etag.d.ts +46 -0
- package/dist/data/row-etag.es.js +64 -0
- package/dist/data/row-etag.es.js.map +1 -0
- package/dist/data/sort-dialect.es.js +236 -0
- package/dist/data/sort-dialect.es.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.es.js +56 -7235
- package/dist/table-classification.es.js +107 -0
- package/dist/table-classification.es.js.map +1 -0
- package/dist/util/auth-default-policies.es.js +184 -0
- package/dist/util/auth-default-policies.es.js.map +1 -0
- package/dist/util/builders.es.js +15 -0
- package/dist/util/builders.es.js.map +1 -0
- package/dist/util/callback-errors.es.js +164 -0
- package/dist/util/callback-errors.es.js.map +1 -0
- package/dist/util/callbacks.es.js +72 -0
- package/dist/util/callbacks.es.js.map +1 -0
- package/dist/util/collections.es.js +79 -0
- package/dist/util/collections.es.js.map +1 -0
- package/dist/util/common.es.js +7 -0
- package/dist/util/common.es.js.map +1 -0
- package/dist/util/conditions.es.js +100 -0
- package/dist/util/conditions.es.js.map +1 -0
- package/dist/util/copy.d.ts +33 -0
- package/dist/util/copy.es.js +74 -0
- package/dist/util/copy.es.js.map +1 -0
- package/dist/util/download-url-cache.d.ts +122 -0
- package/dist/util/download-url-cache.es.js +187 -0
- package/dist/util/download-url-cache.es.js.map +1 -0
- package/dist/util/email.es.js +36 -0
- package/dist/util/email.es.js.map +1 -0
- package/dist/util/entities.d.ts +27 -2
- package/dist/util/entities.es.js +328 -0
- package/dist/util/entities.es.js.map +1 -0
- package/dist/util/enums.d.ts +10 -1
- package/dist/util/enums.es.js +37 -0
- package/dist/util/enums.es.js.map +1 -0
- package/dist/util/identity.es.js +138 -0
- package/dist/util/identity.es.js.map +1 -0
- package/dist/util/index.d.ts +6 -0
- package/dist/util/internal-tables.d.ts +10 -0
- package/dist/util/internal-tables.es.js +202 -0
- package/dist/util/internal-tables.es.js.map +1 -0
- package/dist/util/junction-policies.es.js +273 -0
- package/dist/util/junction-policies.es.js.map +1 -0
- package/dist/util/paths.es.js +26 -0
- package/dist/util/paths.es.js.map +1 -0
- package/dist/util/permissions.es.js +108 -0
- package/dist/util/permissions.es.js.map +1 -0
- package/dist/util/pg-column-to-property.es.js +303 -0
- package/dist/util/pg-column-to-property.es.js.map +1 -0
- package/dist/util/policy/compileRulePolicies.d.ts +32 -0
- package/dist/util/policy/compileRulePolicies.es.js +43 -0
- package/dist/util/policy/compileRulePolicies.es.js.map +1 -0
- package/dist/util/policy/evaluatePolicy.es.js +104 -0
- package/dist/util/policy/evaluatePolicy.es.js.map +1 -0
- package/dist/util/policy/index.d.ts +1 -0
- package/dist/util/policy/policyToPostgres.d.ts +24 -0
- package/dist/util/policy/policyToPostgres.es.js +593 -0
- package/dist/util/policy/policyToPostgres.es.js.map +1 -0
- package/dist/util/policy/policyToSecurityRule.es.js +79 -0
- package/dist/util/policy/policyToSecurityRule.es.js.map +1 -0
- package/dist/util/policy/securityRuleToConditions.es.js +48 -0
- package/dist/util/policy/securityRuleToConditions.es.js.map +1 -0
- package/dist/util/policy/sqlToPolicy.es.js +326 -0
- package/dist/util/policy/sqlToPolicy.es.js.map +1 -0
- package/dist/util/relations.es.js +243 -0
- package/dist/util/relations.es.js.map +1 -0
- package/dist/util/resolutions.es.js +318 -0
- package/dist/util/resolutions.es.js.map +1 -0
- package/dist/util/resolve-relation.es.js +163 -0
- package/dist/util/resolve-relation.es.js.map +1 -0
- package/dist/util/resumable-upload-threshold.d.ts +15 -0
- package/dist/util/resumable-upload-threshold.es.js +20 -0
- package/dist/util/resumable-upload-threshold.es.js.map +1 -0
- package/dist/util/resumable-upload.d.ts +51 -0
- package/dist/util/resumable-upload.es.js +121 -0
- package/dist/util/resumable-upload.es.js.map +1 -0
- package/dist/util/sql-redaction.d.ts +21 -0
- package/dist/util/sql-redaction.es.js +107 -0
- package/dist/util/sql-redaction.es.js.map +1 -0
- package/dist/util/sql-rows.es.js +41 -0
- package/dist/util/sql-rows.es.js.map +1 -0
- package/dist/util/storage.d.ts +26 -4
- package/dist/util/storage.es.js +117 -0
- package/dist/util/storage.es.js.map +1 -0
- package/dist/util/string-column-length.es.js +31 -0
- package/dist/util/string-column-length.es.js.map +1 -0
- package/dist/util/tenant.es.js +201 -0
- package/dist/util/tenant.es.js.map +1 -0
- package/dist/util/untrusted-envelope.d.ts +25 -0
- package/dist/util/untrusted-envelope.es.js +40 -0
- package/dist/util/untrusted-envelope.es.js.map +1 -0
- package/package.json +5 -3
- package/dist/index.es.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-secrets.es.js","names":[],"sources":["../../src/collections/auth-secrets.ts"],"sourcesContent":["import type { Properties } from \"@rebasepro/types\";\nimport { defaultUsersCollection } from \"./default-collections\";\n\n/**\n * Which columns of the user store are the server's alone, and which properties\n * of a given auth collection hold one without saying so.\n *\n * `users` is the one collection a project is expected to redeclare: it is\n * scaffolded into `config/collections/users.ts` so the panel can present it, and\n * the declaration replaces the default outright rather than merging with it. The\n * flag that goes missing unnoticed in that copy is `excludeFromApi` — the\n * neighbouring `admin.hideFromCollection` and `admin.disabled.hidden` keep the\n * field off the screen and look like they did the job.\n *\n * The rule has two readers and they must agree. The server restores the flag so\n * a read does not serve the scrypt hash and a write cannot set it. The panel\n * needs it just as much: a form opens with a value for every property that is\n * not excluded, and submits that baseline, so a panel that does not know sends\n * `passwordHash: null` with every new user and the server refuses the create.\n */\n\n/**\n * What the rule reads off a collection. A `CollectionConfig` is one; so is a\n * collection a server has loaded and not yet validated.\n */\nexport interface AuthSecretCandidate {\n auth?: unknown;\n properties?: Record<string, { excludeFromApi?: boolean; columnName?: string } | undefined>;\n}\n\n/** Is this collection the one the auth subsystem stores users in? */\nfunction isAuthCollection(collection: AuthSecretCandidate): boolean {\n const auth = collection.auth;\n if (auth === true) return true;\n return typeof auth === \"object\" && auth !== null && \"enabled\" in auth && auth.enabled === true;\n}\n\n/**\n * The column names, and property keys, the default users collection marks\n * `excludeFromApi`.\n *\n * Both spellings, because a redeclaration is free to rename the property: one\n * project's copy says `password_hash` where the default says `passwordHash`, and\n * the column is the same either way. Read off {@link defaultUsersCollection}, so\n * a secret added there is covered on the same commit.\n */\nfunction defaultSecretNames(): Set<string> {\n const properties: Properties = defaultUsersCollection.properties;\n const names = new Set<string>();\n for (const [key, property] of Object.entries(properties)) {\n if (!property?.excludeFromApi) continue;\n names.add(key);\n if (property.columnName) names.add(property.columnName);\n }\n return names;\n}\n\n/**\n * The keys of the properties on `collection` that hold an auth secret and do not\n * carry `excludeFromApi`. Empty for any collection that is not the user store: a\n * `password_hash` column elsewhere — a CRM importing hashes, say — is that\n * project's data, not this rule's business.\n */\nexport function authSecretsMissingExclusion(collection: AuthSecretCandidate): string[] {\n if (!collection.properties || !isAuthCollection(collection)) return [];\n\n const secrets = defaultSecretNames();\n const missing: string[] = [];\n for (const [key, property] of Object.entries(collection.properties)) {\n if (!property || property.excludeFromApi) continue;\n if (secrets.has(key) || secrets.has(property.columnName ?? key)) missing.push(key);\n }\n return missing;\n}\n"],"mappings":";;;AA+BA,SAAS,iBAAiB,YAA0C;CAChE,MAAM,OAAO,WAAW;CACxB,IAAI,SAAS,MAAM,OAAO;CAC1B,OAAO,OAAO,SAAS,YAAY,SAAS,QAAQ,aAAa,QAAQ,KAAK,YAAY;AAC9F;;;;;;;;;;AAWA,SAAS,qBAAkC;CACvC,MAAM,aAAyB,uBAAuB;CACtD,MAAM,wBAAQ,IAAI,IAAY;CAC9B,KAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,UAAU,GAAG;EACtD,IAAI,CAAC,UAAU,gBAAgB;EAC/B,MAAM,IAAI,GAAG;EACb,IAAI,SAAS,YAAY,MAAM,IAAI,SAAS,UAAU;CAC1D;CACA,OAAO;AACX;;;;;;;AAQA,SAAgB,4BAA4B,YAA2C;CACnF,IAAI,CAAC,WAAW,cAAc,CAAC,iBAAiB,UAAU,GAAG,OAAO,CAAC;CAErE,MAAM,UAAU,mBAAmB;CACnC,MAAM,UAAoB,CAAC;CAC3B,KAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,WAAW,UAAU,GAAG;EACjE,IAAI,CAAC,YAAY,SAAS,gBAAgB;EAC1C,IAAI,QAAQ,IAAI,GAAG,KAAK,QAAQ,IAAI,SAAS,cAAc,GAAG,GAAG,QAAQ,KAAK,GAAG;CACrF;CACA,OAAO;AACX"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { defineCollection } from "../util/builders.es.js";
|
|
2
|
+
//#region src/collections/default-collections.ts
|
|
3
|
+
/**
|
|
4
|
+
* Default users collection.
|
|
5
|
+
*
|
|
6
|
+
* Prepended to the developer's collections array by the admin and server.
|
|
7
|
+
* Slug-based dedup (Map keyed by slug, last-write-wins) lets developers
|
|
8
|
+
* override by defining their own collection with `slug: "users"`.
|
|
9
|
+
*
|
|
10
|
+
* Schema only — no `admin` block. This package is on the backend's dependency path,
|
|
11
|
+
* where that field does not exist: `@rebasepro/cms-types` adds it by declaration
|
|
12
|
+
* merging, and a BaaS install never installs that. The scaffolded
|
|
13
|
+
* `config/collections/users.ts` carries the presentation for projects that want this
|
|
14
|
+
* collection in their panel, which is also where it is editable.
|
|
15
|
+
*/
|
|
16
|
+
var defaultUsersCollection = defineCollection({
|
|
17
|
+
name: "Users",
|
|
18
|
+
singularName: "User",
|
|
19
|
+
slug: "users",
|
|
20
|
+
auth: true,
|
|
21
|
+
table: "users",
|
|
22
|
+
schema: "rebase",
|
|
23
|
+
securityRules: [{
|
|
24
|
+
operation: "select",
|
|
25
|
+
roles: ["admin"]
|
|
26
|
+
}, {
|
|
27
|
+
operations: [
|
|
28
|
+
"insert",
|
|
29
|
+
"update",
|
|
30
|
+
"delete"
|
|
31
|
+
],
|
|
32
|
+
roles: ["admin"]
|
|
33
|
+
}],
|
|
34
|
+
properties: {
|
|
35
|
+
id: {
|
|
36
|
+
name: "ID",
|
|
37
|
+
type: "string",
|
|
38
|
+
isId: "uuid"
|
|
39
|
+
},
|
|
40
|
+
email: {
|
|
41
|
+
name: "Email",
|
|
42
|
+
type: "string",
|
|
43
|
+
validation: {
|
|
44
|
+
required: true,
|
|
45
|
+
unique: true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
displayName: {
|
|
49
|
+
name: "Name",
|
|
50
|
+
type: "string",
|
|
51
|
+
columnName: "display_name",
|
|
52
|
+
validation: { required: true }
|
|
53
|
+
},
|
|
54
|
+
photoURL: {
|
|
55
|
+
name: "Photo URL",
|
|
56
|
+
type: "string",
|
|
57
|
+
columnName: "photo_url"
|
|
58
|
+
},
|
|
59
|
+
roles: {
|
|
60
|
+
name: "Roles",
|
|
61
|
+
type: "array",
|
|
62
|
+
columnType: "text[]",
|
|
63
|
+
of: {
|
|
64
|
+
name: "Role",
|
|
65
|
+
type: "string",
|
|
66
|
+
enum: {
|
|
67
|
+
admin: "Admin",
|
|
68
|
+
editor: "Editor",
|
|
69
|
+
viewer: "Viewer"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
passwordHash: {
|
|
74
|
+
name: "Password Hash",
|
|
75
|
+
type: "string",
|
|
76
|
+
columnName: "password_hash",
|
|
77
|
+
excludeFromApi: true
|
|
78
|
+
},
|
|
79
|
+
emailVerified: {
|
|
80
|
+
name: "Email Verified",
|
|
81
|
+
type: "boolean",
|
|
82
|
+
columnName: "email_verified",
|
|
83
|
+
defaultValue: false
|
|
84
|
+
},
|
|
85
|
+
emailVerificationToken: {
|
|
86
|
+
name: "Email Verification Token",
|
|
87
|
+
type: "string",
|
|
88
|
+
columnName: "email_verification_token",
|
|
89
|
+
excludeFromApi: true
|
|
90
|
+
},
|
|
91
|
+
emailVerificationSentAt: {
|
|
92
|
+
name: "Email Verification Sent At",
|
|
93
|
+
type: "date",
|
|
94
|
+
columnName: "email_verification_sent_at"
|
|
95
|
+
},
|
|
96
|
+
metadata: {
|
|
97
|
+
name: "Metadata",
|
|
98
|
+
type: "map",
|
|
99
|
+
keyValue: true,
|
|
100
|
+
properties: {},
|
|
101
|
+
defaultValue: {}
|
|
102
|
+
},
|
|
103
|
+
createdAt: {
|
|
104
|
+
name: "Created At",
|
|
105
|
+
type: "date",
|
|
106
|
+
columnName: "created_at",
|
|
107
|
+
autoValue: "on_create"
|
|
108
|
+
},
|
|
109
|
+
updatedAt: {
|
|
110
|
+
name: "Updated At",
|
|
111
|
+
type: "date",
|
|
112
|
+
columnName: "updated_at",
|
|
113
|
+
autoValue: "on_update"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
//#endregion
|
|
118
|
+
export { defaultUsersCollection };
|
|
119
|
+
|
|
120
|
+
//# sourceMappingURL=default-collections.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-collections.es.js","names":[],"sources":["../../src/collections/default-collections.ts"],"sourcesContent":["import { defineCollection } from \"../util/builders\";\n\n/**\n * Default users collection.\n *\n * Prepended to the developer's collections array by the admin and server.\n * Slug-based dedup (Map keyed by slug, last-write-wins) lets developers\n * override by defining their own collection with `slug: \"users\"`.\n *\n * Schema only — no `admin` block. This package is on the backend's dependency path,\n * where that field does not exist: `@rebasepro/cms-types` adds it by declaration\n * merging, and a BaaS install never installs that. The scaffolded\n * `config/collections/users.ts` carries the presentation for projects that want this\n * collection in their panel, which is also where it is editable.\n */\nexport const defaultUsersCollection = defineCollection({\n name: \"Users\",\n singularName: \"User\",\n slug: \"users\",\n auth: true,\n table: \"users\",\n schema: \"rebase\",\n securityRules: [\n { operation: \"select\",\nroles: [\"admin\"] },\n { operations: [\"insert\", \"update\", \"delete\"],\nroles: [\"admin\"] }\n ],\n properties: {\n id: {\n name: \"ID\",\n type: \"string\",\n isId: \"uuid\"\n },\n email: {\n name: \"Email\",\n type: \"string\",\n validation: { required: true,\nunique: true }\n },\n displayName: {\n name: \"Name\",\n type: \"string\",\n columnName: \"display_name\",\n validation: { required: true }\n },\n photoURL: {\n name: \"Photo URL\",\n type: \"string\",\n columnName: \"photo_url\"\n },\n roles: {\n name: \"Roles\",\n type: \"array\",\n columnType: \"text[]\",\n of: {\n name: \"Role\",\n type: \"string\",\n enum: {\n admin: \"Admin\",\n editor: \"Editor\",\n viewer: \"Viewer\"\n }\n }\n },\n passwordHash: {\n name: \"Password Hash\",\n type: \"string\",\n columnName: \"password_hash\",\n excludeFromApi: true\n },\n emailVerified: {\n name: \"Email Verified\",\n type: \"boolean\",\n columnName: \"email_verified\",\n defaultValue: false\n },\n emailVerificationToken: {\n name: \"Email Verification Token\",\n type: \"string\",\n columnName: \"email_verification_token\",\n excludeFromApi: true\n },\n emailVerificationSentAt: {\n name: \"Email Verification Sent At\",\n type: \"date\",\n columnName: \"email_verification_sent_at\"\n },\n metadata: {\n name: \"Metadata\",\n type: \"map\",\n keyValue: true,\n properties: {},\n defaultValue: {}\n },\n createdAt: {\n name: \"Created At\",\n type: \"date\",\n columnName: \"created_at\",\n autoValue: \"on_create\"\n },\n updatedAt: {\n name: \"Updated At\",\n type: \"date\",\n columnName: \"updated_at\",\n autoValue: \"on_update\"\n }\n }\n});\n"],"mappings":";;;;;;;;;;;;;;;AAeA,IAAa,yBAAyB,iBAAiB;CACnD,MAAM;CACN,cAAc;CACd,MAAM;CACN,MAAM;CACN,OAAO;CACP,QAAQ;CACR,eAAe,CACX;EAAE,WAAW;EACrB,OAAO,CAAC,OAAO;CAAE,GACT;EAAE,YAAY;GAAC;GAAU;GAAU;EAAQ;EACnD,OAAO,CAAC,OAAO;CAAE,CACb;CACA,YAAY;EACR,IAAI;GACA,MAAM;GACN,MAAM;GACN,MAAM;EACV;EACA,OAAO;GACH,MAAM;GACN,MAAM;GACN,YAAY;IAAE,UAAU;IACpC,QAAQ;GAAK;EACL;EACA,aAAa;GACT,MAAM;GACN,MAAM;GACN,YAAY;GACZ,YAAY,EAAE,UAAU,KAAK;EACjC;EACA,UAAU;GACN,MAAM;GACN,MAAM;GACN,YAAY;EAChB;EACA,OAAO;GACH,MAAM;GACN,MAAM;GACN,YAAY;GACZ,IAAI;IACA,MAAM;IACN,MAAM;IACN,MAAM;KACF,OAAO;KACP,QAAQ;KACR,QAAQ;IACZ;GACJ;EACJ;EACA,cAAc;GACV,MAAM;GACN,MAAM;GACN,YAAY;GACZ,gBAAgB;EACpB;EACA,eAAe;GACX,MAAM;GACN,MAAM;GACN,YAAY;GACZ,cAAc;EAClB;EACA,wBAAwB;GACpB,MAAM;GACN,MAAM;GACN,YAAY;GACZ,gBAAgB;EACpB;EACA,yBAAyB;GACrB,MAAM;GACN,MAAM;GACN,YAAY;EAChB;EACA,UAAU;GACN,MAAM;GACN,MAAM;GACN,UAAU;GACV,YAAY,CAAC;GACb,cAAc,CAAC;EACnB;EACA,WAAW;GACP,MAAM;GACN,MAAM;GACN,YAAY;GACZ,WAAW;EACf;EACA,WAAW;GACP,MAAM;GACN,MAAM;GACN,YAAY;GACZ,WAAW;EACf;CACJ;AACJ,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isPostgresCollectionConfig } from "@rebasepro/types";
|
|
2
|
+
//#region src/collections/engine-support.ts
|
|
3
|
+
/**
|
|
4
|
+
* Refuse a `beforeQuery` hook on a collection Postgres does not serve.
|
|
5
|
+
*
|
|
6
|
+
* `beforeQuery` narrows a read *before it is compiled*, which means compiling
|
|
7
|
+
* a declarative filter into the query the engine is about to run. Only
|
|
8
|
+
* `@rebasepro/server-postgres` does that today. On any other engine the hook
|
|
9
|
+
* would look configured and do nothing — and for a hook whose whole purpose is
|
|
10
|
+
* to restrict which rows come back, "nothing" means every row served to
|
|
11
|
+
* everybody.
|
|
12
|
+
*
|
|
13
|
+
* So it is a boot failure, named. The alternative — declaring the hook only on
|
|
14
|
+
* `PostgresCollectionConfig`, the way `search` is — is not available:
|
|
15
|
+
* `callbacks` is one engine-agnostic type shared by every collection, and
|
|
16
|
+
* splitting it would fork `afterRead` and the five write hooks along with it.
|
|
17
|
+
*
|
|
18
|
+
* Lives here rather than in either driver because both have to run it: the
|
|
19
|
+
* Postgres schema planner sees every collection in a mixed app, and a
|
|
20
|
+
* Mongo-only app never boots a Postgres planner at all.
|
|
21
|
+
*
|
|
22
|
+
* Called with *every* collection, before an engine filters any out.
|
|
23
|
+
*/
|
|
24
|
+
var assertBeforeQueryIsPostgresOnly = (collections) => {
|
|
25
|
+
for (const collection of collections) {
|
|
26
|
+
if (isPostgresCollectionConfig(collection)) continue;
|
|
27
|
+
if (!collection.callbacks?.beforeQuery) continue;
|
|
28
|
+
const engine = collection.engine ?? "non-postgres";
|
|
29
|
+
throw new Error(`${collection.slug}.callbacks.beforeQuery: narrowing a read before it is compiled is a Postgres feature, and this collection is served by \`${engine}\`. Remove the hook — it would otherwise look configured while every read kept returning every row the engine allows. Redaction that works on every engine is \`afterRead\`.`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { assertBeforeQueryIsPostgresOnly };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=engine-support.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine-support.es.js","names":[],"sources":["../../src/collections/engine-support.ts"],"sourcesContent":["import type { CollectionConfig } from \"@rebasepro/types\";\nimport { isPostgresCollectionConfig } from \"@rebasepro/types\";\n\n/**\n * Refuse a `beforeQuery` hook on a collection Postgres does not serve.\n *\n * `beforeQuery` narrows a read *before it is compiled*, which means compiling\n * a declarative filter into the query the engine is about to run. Only\n * `@rebasepro/server-postgres` does that today. On any other engine the hook\n * would look configured and do nothing — and for a hook whose whole purpose is\n * to restrict which rows come back, \"nothing\" means every row served to\n * everybody.\n *\n * So it is a boot failure, named. The alternative — declaring the hook only on\n * `PostgresCollectionConfig`, the way `search` is — is not available:\n * `callbacks` is one engine-agnostic type shared by every collection, and\n * splitting it would fork `afterRead` and the five write hooks along with it.\n *\n * Lives here rather than in either driver because both have to run it: the\n * Postgres schema planner sees every collection in a mixed app, and a\n * Mongo-only app never boots a Postgres planner at all.\n *\n * Called with *every* collection, before an engine filters any out.\n */\nexport const assertBeforeQueryIsPostgresOnly = (collections: CollectionConfig[]): void => {\n for (const collection of collections) {\n if (isPostgresCollectionConfig(collection)) continue;\n if (!collection.callbacks?.beforeQuery) continue;\n const engine = collection.engine ?? \"non-postgres\";\n throw new Error(\n `${collection.slug}.callbacks.beforeQuery: narrowing a read before it is compiled is a Postgres ` +\n `feature, and this collection is served by \\`${engine}\\`. Remove the hook — it would otherwise ` +\n \"look configured while every read kept returning every row the engine allows. Redaction that \" +\n \"works on every engine is `afterRead`.\"\n );\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwBA,IAAa,mCAAmC,gBAA0C;CACtF,KAAK,MAAM,cAAc,aAAa;EAClC,IAAI,2BAA2B,UAAU,GAAG;EAC5C,IAAI,CAAC,WAAW,WAAW,aAAa;EACxC,MAAM,SAAS,WAAW,UAAU;EACpC,MAAM,IAAI,MACN,GAAG,WAAW,KAAK,2HAC4B,OAAO,6KAG1D;CACJ;AACJ"}
|
|
@@ -71,6 +71,19 @@ export declare function restrictedFieldNames(collection: CollectionConfig, viewe
|
|
|
71
71
|
declared: string[];
|
|
72
72
|
refused: Set<string>;
|
|
73
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* The names on this collection that the API does not have, in every spelling
|
|
76
|
+
* a caller could write them in — the `excludeFromApi` properties, their column
|
|
77
|
+
* names, and the foreign key of an excluded to-one relation.
|
|
78
|
+
*
|
|
79
|
+
* Not the same set as {@link restrictedFieldNames}. A field restricted by role
|
|
80
|
+
* is public by name: OpenAPI and `list_collections` describe it, and refusing a
|
|
81
|
+
* query on it with "not readable" tells the caller nothing they could not read
|
|
82
|
+
* there. An `excludeFromApi` field is described nowhere, so a query naming one
|
|
83
|
+
* has to be answered as a query naming a column that does not exist — anything
|
|
84
|
+
* else confirms that it does.
|
|
85
|
+
*/
|
|
86
|
+
export declare function apiHiddenFieldNames(collection: CollectionConfig): Set<string>;
|
|
74
87
|
/**
|
|
75
88
|
* True when nothing on this collection restricts a field, for either direction.
|
|
76
89
|
*
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { fieldKeyForColumn, findRelation, resolveCollectionRelations } from "../util/relations.es.js";
|
|
2
|
+
//#region src/collections/field-access.ts
|
|
3
|
+
/**
|
|
4
|
+
* The role that satisfies any non-empty list.
|
|
5
|
+
*
|
|
6
|
+
* The same arm every baseline policy carries: `security_rules` injects
|
|
7
|
+
* `rolesOverlap(['admin'])` into the default read and write policies, and
|
|
8
|
+
* `rebase.dataAsAdmin` is scoped with `{ uid: "service", roles: ["admin"] }`.
|
|
9
|
+
* Without this an author could declare `access: { read: ["hr"] }` and lock the
|
|
10
|
+
* administrator out of a column of their own database — and lock the Studio out
|
|
11
|
+
* of rendering it.
|
|
12
|
+
*/
|
|
13
|
+
var ADMIN_ROLE = "admin";
|
|
14
|
+
/**
|
|
15
|
+
* What a property's access rules actually are, with `excludeFromApi` expanded.
|
|
16
|
+
*
|
|
17
|
+
* Returns `undefined` when the property constrains nothing, so callers can skip
|
|
18
|
+
* the whole check for the overwhelmingly common case.
|
|
19
|
+
*/
|
|
20
|
+
function effectiveAccess(property) {
|
|
21
|
+
if (!property) return void 0;
|
|
22
|
+
if (property.excludeFromApi) return EXCLUDED_ACCESS;
|
|
23
|
+
const access = property.access;
|
|
24
|
+
if (!access) return void 0;
|
|
25
|
+
if (access.read === void 0 && access.write === void 0) return void 0;
|
|
26
|
+
return access;
|
|
27
|
+
}
|
|
28
|
+
/** The rule `excludeFromApi: true` expands to. Frozen: it is shared by every caller. */
|
|
29
|
+
var EXCLUDED_ACCESS = Object.freeze({
|
|
30
|
+
read: Object.freeze([]),
|
|
31
|
+
write: Object.freeze([])
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Does a caller holding `roles` satisfy `allowed`?
|
|
35
|
+
*
|
|
36
|
+
* Three cases, and the middle one is the one worth stating out loud:
|
|
37
|
+
*
|
|
38
|
+
* - `allowed` omitted — the field carries no rule of its own, so the row's
|
|
39
|
+
* policies have already answered. True.
|
|
40
|
+
* - `allowed` empty — nobody, at any privilege, through any API. Not the admin,
|
|
41
|
+
* not the service key, not the trusted plane reading on a caller's behalf.
|
|
42
|
+
* This is what `excludeFromApi` has always meant on the read side, and
|
|
43
|
+
* collapsing the two spellings means the empty list has to keep meaning it.
|
|
44
|
+
* - `allowed` non-empty — one of the named roles, or `admin`, or no viewer at
|
|
45
|
+
* all (the trusted server plane, which is not an API caller).
|
|
46
|
+
*/
|
|
47
|
+
function satisfies(allowed, viewer) {
|
|
48
|
+
if (allowed === void 0) return true;
|
|
49
|
+
if (allowed.length === 0) return false;
|
|
50
|
+
if (!viewer) return true;
|
|
51
|
+
const roles = viewer.roles;
|
|
52
|
+
if (!roles || roles.length === 0) return false;
|
|
53
|
+
return roles.includes(ADMIN_ROLE) || allowed.some((role) => roles.includes(role));
|
|
54
|
+
}
|
|
55
|
+
/** May this caller receive this field's value? */
|
|
56
|
+
function canReadField(property, viewer) {
|
|
57
|
+
const access = effectiveAccess(property);
|
|
58
|
+
return access ? satisfies(access.read, viewer) : true;
|
|
59
|
+
}
|
|
60
|
+
/** May this caller set this field's value? */
|
|
61
|
+
function canWriteField(property, viewer) {
|
|
62
|
+
const access = effectiveAccess(property);
|
|
63
|
+
return access ? satisfies(access.write, viewer) : true;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The names on this collection a caller may not touch, in the two spellings a
|
|
67
|
+
* caller can write them in.
|
|
68
|
+
*
|
|
69
|
+
* `declared` is the property keys, which is what has to leave a *known-fields*
|
|
70
|
+
* set. `refused` is those plus the physical column names behind them: a caller
|
|
71
|
+
* who knows the table can send `password_hash` as readily as `passwordHash`, and
|
|
72
|
+
* a rule that only knew the wire name would be one rename away from useless.
|
|
73
|
+
*
|
|
74
|
+
* A restricted to-one relation takes its foreign key with it. `bandId: 7` names
|
|
75
|
+
* the band exactly as `band: { id: 7 }` does, and the row carries the column
|
|
76
|
+
* beside the relation — so a rule on the relation that left the key alone
|
|
77
|
+
* withheld the value from the row and still let `?bandId=7` ask for it.
|
|
78
|
+
*
|
|
79
|
+
* `kind` picks which half of the rule is read; nothing else differs.
|
|
80
|
+
*/
|
|
81
|
+
function restrictedFieldNames(collection, viewer, kind) {
|
|
82
|
+
const allowed = kind === "read" ? canReadField : canWriteField;
|
|
83
|
+
return namesWhere(collection, (property) => !allowed(property, viewer));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The names on this collection that the API does not have, in every spelling
|
|
87
|
+
* a caller could write them in — the `excludeFromApi` properties, their column
|
|
88
|
+
* names, and the foreign key of an excluded to-one relation.
|
|
89
|
+
*
|
|
90
|
+
* Not the same set as {@link restrictedFieldNames}. A field restricted by role
|
|
91
|
+
* is public by name: OpenAPI and `list_collections` describe it, and refusing a
|
|
92
|
+
* query on it with "not readable" tells the caller nothing they could not read
|
|
93
|
+
* there. An `excludeFromApi` field is described nowhere, so a query naming one
|
|
94
|
+
* has to be answered as a query naming a column that does not exist — anything
|
|
95
|
+
* else confirms that it does.
|
|
96
|
+
*/
|
|
97
|
+
function apiHiddenFieldNames(collection) {
|
|
98
|
+
return namesWhere(collection, (property) => Boolean(property?.excludeFromApi)).refused;
|
|
99
|
+
}
|
|
100
|
+
function namesWhere(collection, restricted) {
|
|
101
|
+
const declared = [];
|
|
102
|
+
const refused = /* @__PURE__ */ new Set();
|
|
103
|
+
const properties = collection.properties ?? {};
|
|
104
|
+
let relations;
|
|
105
|
+
const restrict = (name) => {
|
|
106
|
+
if (name in properties && !declared.includes(name)) declared.push(name);
|
|
107
|
+
refused.add(name);
|
|
108
|
+
};
|
|
109
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
110
|
+
if (!restricted(property)) continue;
|
|
111
|
+
restrict(name);
|
|
112
|
+
const columnName = property.columnName;
|
|
113
|
+
if (columnName) refused.add(columnName);
|
|
114
|
+
if (property.type === "relation") {
|
|
115
|
+
relations ??= resolveCollectionRelations(collection);
|
|
116
|
+
const relation = findRelation(relations, name);
|
|
117
|
+
if (relation?.kind === "belongsTo") {
|
|
118
|
+
restrict(fieldKeyForColumn(collection, relation.localKey));
|
|
119
|
+
refused.add(relation.localKey);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
declared,
|
|
125
|
+
refused
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* True when nothing on this collection restricts a field, for either direction.
|
|
130
|
+
*
|
|
131
|
+
* Every read of every row runs through the strip, so the collection that has no
|
|
132
|
+
* rules — which is almost all of them — has to cost one property walk and no
|
|
133
|
+
* allocation.
|
|
134
|
+
*/
|
|
135
|
+
function hasFieldAccessRules(collection) {
|
|
136
|
+
for (const property of Object.values(collection.properties ?? {})) if (effectiveAccess(property)) return true;
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
export { ADMIN_ROLE, apiHiddenFieldNames, canReadField, canWriteField, effectiveAccess, hasFieldAccessRules, restrictedFieldNames };
|
|
141
|
+
|
|
142
|
+
//# sourceMappingURL=field-access.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-access.es.js","names":[],"sources":["../../src/collections/field-access.ts"],"sourcesContent":["import type { CollectionConfig, FieldAccess, Property } from \"@rebasepro/types\";\nimport { fieldKeyForColumn, findRelation, resolveCollectionRelations } from \"../util/relations\";\n\n/**\n * Field-level access control: one mechanism, read by every enforcement point.\n *\n * A collection's `securityRules` decide which *rows* a caller reaches;\n * `property.access` decides which *fields* of a reached row they see and may\n * set. The two are independent — a field rule never widens row access, and a\n * row a caller cannot read has no fields to talk about.\n *\n * `excludeFromApi` is sugar for `access: { read: [], write: [] }` and is\n * normalised into it by {@link effectiveAccess}, which is the only place either\n * spelling is read. It used to be its own code path in five files — the read\n * strip, the write refusal, the SDK generator, the OpenAPI schema builder and\n * the filter-parameter builder — and the second rule would have made ten.\n * There is one predicate now, and the flag is a shorthand for it.\n *\n * @module\n */\n\n/**\n * The caller a field rule is judged against: whatever the call context carries\n * as the user's application roles.\n *\n * `undefined` is the trusted server plane — an in-process `rebase.data` call\n * with no request behind it, the auth adapter writing a password hash, a\n * migration. Every API boundary has a viewer: an unauthenticated REST request is\n * scoped as `{ uid: ANONYMOUS_USER_ID, roles: [\"anon\"] }` before it reaches a\n * driver, so \"no viewer\" cannot be reached from outside.\n */\nexport interface FieldViewer {\n roles?: readonly string[];\n}\n\n/**\n * The role that satisfies any non-empty list.\n *\n * The same arm every baseline policy carries: `security_rules` injects\n * `rolesOverlap(['admin'])` into the default read and write policies, and\n * `rebase.dataAsAdmin` is scoped with `{ uid: \"service\", roles: [\"admin\"] }`.\n * Without this an author could declare `access: { read: [\"hr\"] }` and lock the\n * administrator out of a column of their own database — and lock the Studio out\n * of rendering it.\n */\nexport const ADMIN_ROLE = \"admin\";\n\n/**\n * What a property's access rules actually are, with `excludeFromApi` expanded.\n *\n * Returns `undefined` when the property constrains nothing, so callers can skip\n * the whole check for the overwhelmingly common case.\n */\nexport function effectiveAccess(property: Property | undefined): FieldAccess | undefined {\n if (!property) return undefined;\n if (property.excludeFromApi) return EXCLUDED_ACCESS;\n const access = property.access;\n if (!access) return undefined;\n if (access.read === undefined && access.write === undefined) return undefined;\n return access;\n}\n\n/** The rule `excludeFromApi: true` expands to. Frozen: it is shared by every caller. */\nconst EXCLUDED_ACCESS: FieldAccess = Object.freeze({ read: Object.freeze([]), write: Object.freeze([]) });\n\n/**\n * Does a caller holding `roles` satisfy `allowed`?\n *\n * Three cases, and the middle one is the one worth stating out loud:\n *\n * - `allowed` omitted — the field carries no rule of its own, so the row's\n * policies have already answered. True.\n * - `allowed` empty — nobody, at any privilege, through any API. Not the admin,\n * not the service key, not the trusted plane reading on a caller's behalf.\n * This is what `excludeFromApi` has always meant on the read side, and\n * collapsing the two spellings means the empty list has to keep meaning it.\n * - `allowed` non-empty — one of the named roles, or `admin`, or no viewer at\n * all (the trusted server plane, which is not an API caller).\n */\nfunction satisfies(allowed: readonly string[] | undefined, viewer: FieldViewer | undefined): boolean {\n if (allowed === undefined) return true;\n if (allowed.length === 0) return false;\n if (!viewer) return true;\n const roles = viewer.roles;\n if (!roles || roles.length === 0) return false;\n return roles.includes(ADMIN_ROLE) || allowed.some(role => roles.includes(role));\n}\n\n/** May this caller receive this field's value? */\nexport function canReadField(property: Property | undefined, viewer: FieldViewer | undefined): boolean {\n const access = effectiveAccess(property);\n return access ? satisfies(access.read, viewer) : true;\n}\n\n/** May this caller set this field's value? */\nexport function canWriteField(property: Property | undefined, viewer: FieldViewer | undefined): boolean {\n const access = effectiveAccess(property);\n return access ? satisfies(access.write, viewer) : true;\n}\n\n/**\n * The names on this collection a caller may not touch, in the two spellings a\n * caller can write them in.\n *\n * `declared` is the property keys, which is what has to leave a *known-fields*\n * set. `refused` is those plus the physical column names behind them: a caller\n * who knows the table can send `password_hash` as readily as `passwordHash`, and\n * a rule that only knew the wire name would be one rename away from useless.\n *\n * A restricted to-one relation takes its foreign key with it. `bandId: 7` names\n * the band exactly as `band: { id: 7 }` does, and the row carries the column\n * beside the relation — so a rule on the relation that left the key alone\n * withheld the value from the row and still let `?bandId=7` ask for it.\n *\n * `kind` picks which half of the rule is read; nothing else differs.\n */\nexport function restrictedFieldNames(\n collection: CollectionConfig,\n viewer: FieldViewer | undefined,\n kind: \"read\" | \"write\"\n): { declared: string[]; refused: Set<string> } {\n const allowed = kind === \"read\" ? canReadField : canWriteField;\n return namesWhere(collection, property => !allowed(property, viewer));\n}\n\n/**\n * The names on this collection that the API does not have, in every spelling\n * a caller could write them in — the `excludeFromApi` properties, their column\n * names, and the foreign key of an excluded to-one relation.\n *\n * Not the same set as {@link restrictedFieldNames}. A field restricted by role\n * is public by name: OpenAPI and `list_collections` describe it, and refusing a\n * query on it with \"not readable\" tells the caller nothing they could not read\n * there. An `excludeFromApi` field is described nowhere, so a query naming one\n * has to be answered as a query naming a column that does not exist — anything\n * else confirms that it does.\n */\nexport function apiHiddenFieldNames(collection: CollectionConfig): Set<string> {\n return namesWhere(collection, property => Boolean(property?.excludeFromApi)).refused;\n}\n\nfunction namesWhere(\n collection: CollectionConfig,\n restricted: (property: Property) => boolean\n): { declared: string[]; refused: Set<string> } {\n const declared: string[] = [];\n const refused = new Set<string>();\n const properties = collection.properties ?? {};\n // Resolved only when a restricted property is a relation, which is rare:\n // the read strip runs this on every row.\n let relations: ReturnType<typeof resolveCollectionRelations> | undefined;\n\n const restrict = (name: string) => {\n if (name in properties && !declared.includes(name)) declared.push(name);\n refused.add(name);\n };\n\n for (const [name, property] of Object.entries(properties)) {\n if (!restricted(property as Property)) continue;\n restrict(name);\n const columnName = (property as Property).columnName;\n if (columnName) refused.add(columnName);\n if ((property as Property).type === \"relation\") {\n relations ??= resolveCollectionRelations(collection);\n const relation = findRelation(relations, name);\n if (relation?.kind === \"belongsTo\") {\n restrict(fieldKeyForColumn(collection, relation.localKey));\n refused.add(relation.localKey);\n }\n }\n }\n return { declared, refused };\n}\n\n/**\n * True when nothing on this collection restricts a field, for either direction.\n *\n * Every read of every row runs through the strip, so the collection that has no\n * rules — which is almost all of them — has to cost one property walk and no\n * allocation.\n */\nexport function hasFieldAccessRules(collection: CollectionConfig): boolean {\n for (const property of Object.values(collection.properties ?? {})) {\n if (effectiveAccess(property as Property)) return true;\n }\n return false;\n}\n"],"mappings":";;;;;;;;;;;;AA6CA,IAAa,aAAa;;;;;;;AAQ1B,SAAgB,gBAAgB,UAAyD;CACrF,IAAI,CAAC,UAAU,OAAO,KAAA;CACtB,IAAI,SAAS,gBAAgB,OAAO;CACpC,MAAM,SAAS,SAAS;CACxB,IAAI,CAAC,QAAQ,OAAO,KAAA;CACpB,IAAI,OAAO,SAAS,KAAA,KAAa,OAAO,UAAU,KAAA,GAAW,OAAO,KAAA;CACpE,OAAO;AACX;;AAGA,IAAM,kBAA+B,OAAO,OAAO;CAAE,MAAM,OAAO,OAAO,CAAC,CAAC;CAAG,OAAO,OAAO,OAAO,CAAC,CAAC;AAAE,CAAC;;;;;;;;;;;;;;;AAgBxG,SAAS,UAAU,SAAwC,QAA0C;CACjG,IAAI,YAAY,KAAA,GAAW,OAAO;CAClC,IAAI,QAAQ,WAAW,GAAG,OAAO;CACjC,IAAI,CAAC,QAAQ,OAAO;CACpB,MAAM,QAAQ,OAAO;CACrB,IAAI,CAAC,SAAS,MAAM,WAAW,GAAG,OAAO;CACzC,OAAO,MAAM,SAAS,UAAU,KAAK,QAAQ,MAAK,SAAQ,MAAM,SAAS,IAAI,CAAC;AAClF;;AAGA,SAAgB,aAAa,UAAgC,QAA0C;CACnG,MAAM,SAAS,gBAAgB,QAAQ;CACvC,OAAO,SAAS,UAAU,OAAO,MAAM,MAAM,IAAI;AACrD;;AAGA,SAAgB,cAAc,UAAgC,QAA0C;CACpG,MAAM,SAAS,gBAAgB,QAAQ;CACvC,OAAO,SAAS,UAAU,OAAO,OAAO,MAAM,IAAI;AACtD;;;;;;;;;;;;;;;;;AAkBA,SAAgB,qBACZ,YACA,QACA,MAC4C;CAC5C,MAAM,UAAU,SAAS,SAAS,eAAe;CACjD,OAAO,WAAW,aAAY,aAAY,CAAC,QAAQ,UAAU,MAAM,CAAC;AACxE;;;;;;;;;;;;;AAcA,SAAgB,oBAAoB,YAA2C;CAC3E,OAAO,WAAW,aAAY,aAAY,QAAQ,UAAU,cAAc,CAAC,CAAC,CAAC;AACjF;AAEA,SAAS,WACL,YACA,YAC4C;CAC5C,MAAM,WAAqB,CAAC;CAC5B,MAAM,0BAAU,IAAI,IAAY;CAChC,MAAM,aAAa,WAAW,cAAc,CAAC;CAG7C,IAAI;CAEJ,MAAM,YAAY,SAAiB;EAC/B,IAAI,QAAQ,cAAc,CAAC,SAAS,SAAS,IAAI,GAAG,SAAS,KAAK,IAAI;EACtE,QAAQ,IAAI,IAAI;CACpB;CAEA,KAAK,MAAM,CAAC,MAAM,aAAa,OAAO,QAAQ,UAAU,GAAG;EACvD,IAAI,CAAC,WAAW,QAAoB,GAAG;EACvC,SAAS,IAAI;EACb,MAAM,aAAc,SAAsB;EAC1C,IAAI,YAAY,QAAQ,IAAI,UAAU;EACtC,IAAK,SAAsB,SAAS,YAAY;GAC5C,cAAc,2BAA2B,UAAU;GACnD,MAAM,WAAW,aAAa,WAAW,IAAI;GAC7C,IAAI,UAAU,SAAS,aAAa;IAChC,SAAS,kBAAkB,YAAY,SAAS,QAAQ,CAAC;IACzD,QAAQ,IAAI,SAAS,QAAQ;GACjC;EACJ;CACJ;CACA,OAAO;EAAE;EAAU;CAAQ;AAC/B;;;;;;;;AASA,SAAgB,oBAAoB,YAAuC;CACvE,KAAK,MAAM,YAAY,OAAO,OAAO,WAAW,cAAc,CAAC,CAAC,GAC5D,IAAI,gBAAgB,QAAoB,GAAG,OAAO;CAEtD,OAAO;AACX"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/collections/soft-delete.ts
|
|
2
|
+
/** The field `softDelete: true` means, when the object form names none. */
|
|
3
|
+
var DEFAULT_SOFT_DELETE_FIELD = "deletedAt";
|
|
4
|
+
/**
|
|
5
|
+
* The property a `softDelete` collection records a deletion in — its wire
|
|
6
|
+
* name, what a delete stamps and what a restore (an update to `null`) clears —
|
|
7
|
+
* or `undefined` for a collection without soft delete.
|
|
8
|
+
*
|
|
9
|
+
* One reading of the declaration for every layer that needs it: the driver
|
|
10
|
+
* that stamps and filters on it, and the REST route that has to recognise a
|
|
11
|
+
* restore before it looks the row up.
|
|
12
|
+
*/
|
|
13
|
+
function softDeleteFieldOf(collection) {
|
|
14
|
+
const declared = collection && "softDelete" in collection ? collection.softDelete : void 0;
|
|
15
|
+
if (!declared) return void 0;
|
|
16
|
+
return typeof declared === "object" && declared.field || DEFAULT_SOFT_DELETE_FIELD;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether an update's values restore a soft-deleted row: they set the
|
|
20
|
+
* soft-delete field back to `null`, the documented restore.
|
|
21
|
+
*
|
|
22
|
+
* A restore is an ordinary update of a row every default read hides, so a door
|
|
23
|
+
* that looks the row up before updating it has to include the trashed rows for
|
|
24
|
+
* this update — and only for this one: any other edit of a trashed row is a
|
|
25
|
+
* 404.
|
|
26
|
+
*/
|
|
27
|
+
function restoresSoftDeletedRow(collection, values) {
|
|
28
|
+
const field = softDeleteFieldOf(collection);
|
|
29
|
+
return field !== void 0 && values?.[field] === null;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { DEFAULT_SOFT_DELETE_FIELD, restoresSoftDeletedRow, softDeleteFieldOf };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=soft-delete.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"soft-delete.es.js","names":[],"sources":["../../src/collections/soft-delete.ts"],"sourcesContent":["import type { CollectionConfig } from \"@rebasepro/types\";\n\n/** The field `softDelete: true` means, when the object form names none. */\nexport const DEFAULT_SOFT_DELETE_FIELD = \"deletedAt\";\n\n/**\n * The property a `softDelete` collection records a deletion in — its wire\n * name, what a delete stamps and what a restore (an update to `null`) clears —\n * or `undefined` for a collection without soft delete.\n *\n * One reading of the declaration for every layer that needs it: the driver\n * that stamps and filters on it, and the REST route that has to recognise a\n * restore before it looks the row up.\n */\nexport function softDeleteFieldOf(collection: CollectionConfig | undefined): string | undefined {\n // Declared on the Postgres collection shape only; narrowed, not cast.\n const declared = collection && \"softDelete\" in collection ? collection.softDelete : undefined;\n if (!declared) return undefined;\n return (typeof declared === \"object\" && declared.field) || DEFAULT_SOFT_DELETE_FIELD;\n}\n\n/**\n * Whether an update's values restore a soft-deleted row: they set the\n * soft-delete field back to `null`, the documented restore.\n *\n * A restore is an ordinary update of a row every default read hides, so a door\n * that looks the row up before updating it has to include the trashed rows for\n * this update — and only for this one: any other edit of a trashed row is a\n * 404.\n */\nexport function restoresSoftDeletedRow(\n collection: CollectionConfig | undefined,\n values: Record<string, unknown> | undefined\n): boolean {\n const field = softDeleteFieldOf(collection);\n return field !== undefined && values?.[field] === null;\n}\n"],"mappings":";;AAGA,IAAa,4BAA4B;;;;;;;;;;AAWzC,SAAgB,kBAAkB,YAA8D;CAE5F,MAAM,WAAW,cAAc,gBAAgB,aAAa,WAAW,aAAa,KAAA;CACpF,IAAI,CAAC,UAAU,OAAO,KAAA;CACtB,OAAQ,OAAO,aAAa,YAAY,SAAS,SAAU;AAC/D;;;;;;;;;;AAWA,SAAgB,uBACZ,YACA,QACO;CACP,MAAM,QAAQ,kBAAkB,UAAU;CAC1C,OAAO,UAAU,KAAA,KAAa,SAAS,WAAW;AACtD"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CollectionConfig } from "@rebasepro/types";
|
|
2
|
+
/**
|
|
3
|
+
* The answers a query gets for naming a field that does not exist — written
|
|
4
|
+
* once, here, because two layers give them.
|
|
5
|
+
*
|
|
6
|
+
* The driver gives them for a typo. The REST check gives the same ones for a
|
|
7
|
+
* field marked `excludeFromApi`: OpenAPI, `list_collections` and the generated
|
|
8
|
+
* SDK all leave such a field out, so anything but the typo's answer — "not
|
|
9
|
+
* readable", a different message, a list that names it — confirms it exists.
|
|
10
|
+
* Both build the answer from these functions, so the two cannot drift apart by
|
|
11
|
+
* a byte. Each returns the parts of the 400 (`message`, `code`, `details`) and
|
|
12
|
+
* the caller wraps them in its own error type.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
export interface UnknownFieldAnswer {
|
|
17
|
+
message: string;
|
|
18
|
+
code: string;
|
|
19
|
+
details: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The fields an unknown-field answer offers as valid, sorted: what a query on
|
|
23
|
+
* this collection can name as a column. A relation contributes its foreign key
|
|
24
|
+
* (`authorId`) when the key is on this table, and nothing otherwise. An
|
|
25
|
+
* `excludeFromApi` field is never offered.
|
|
26
|
+
*
|
|
27
|
+
* Read off the collection rather than the table, so the REST check — which
|
|
28
|
+
* holds no table — lists exactly what the driver lists.
|
|
29
|
+
*/
|
|
30
|
+
export declare function queryableFieldNames(collection: CollectionConfig): string[];
|
|
31
|
+
/** A filter key that resolves to nothing on `collectionPath`. */
|
|
32
|
+
export declare function unknownFilterField(field: string, collectionPath: string, validFields: string[]): UnknownFieldAnswer;
|
|
33
|
+
/** `author.emial` — a relation path whose last segment names nothing on the relation's target. */
|
|
34
|
+
export declare function unknownRelationTargetField(path: {
|
|
35
|
+
field: string;
|
|
36
|
+
relationKey: string;
|
|
37
|
+
fieldKey: string;
|
|
38
|
+
}, target: {
|
|
39
|
+
slug: string;
|
|
40
|
+
validFields: string[];
|
|
41
|
+
}, collectionPath: string): UnknownFieldAnswer;
|
|
42
|
+
/** An `orderBy` key that resolves to nothing. `collectionName` is absent when the read carries no collection. */
|
|
43
|
+
export declare function unknownOrderByField(orderBy: string, collectionName: string | undefined, validFields: string[]): UnknownFieldAnswer;
|
|
44
|
+
/** A `fields` entry that names nothing. */
|
|
45
|
+
export declare function unknownProjectionField(name: string, collectionName: string, validFields: string[]): UnknownFieldAnswer;
|
|
46
|
+
/**
|
|
47
|
+
* An `include` naming no relation of the collection. `validRelations` is what
|
|
48
|
+
* the collection declares, never an `excludeFromApi` relation.
|
|
49
|
+
*/
|
|
50
|
+
export declare function unknownIncludeRelation(key: string, collectionName: string, validRelations: string[]): UnknownFieldAnswer;
|
|
51
|
+
/** The relations an unknown-relation answer offers, sorted — never an `excludeFromApi` one. */
|
|
52
|
+
export declare function includableRelationNames(collection: CollectionConfig): string[];
|
|
53
|
+
/**
|
|
54
|
+
* An aggregate's `select` field or `groupBy` key that names no column.
|
|
55
|
+
* `forWhat` is where it was named: `sum()`, `count()`, `groupBy`.
|
|
56
|
+
* `validFields` is {@link queryableFieldNames}, never an `excludeFromApi` field.
|
|
57
|
+
*/
|
|
58
|
+
export declare function unknownAggregateField(field: string, forWhat: string, validFields: string[]): UnknownFieldAnswer;
|
|
59
|
+
/**
|
|
60
|
+
* The properties a `vector_search` can name, sorted: those declared
|
|
61
|
+
* `{ type: "vector" }`, never an `excludeFromApi` one.
|
|
62
|
+
*/
|
|
63
|
+
export declare function vectorPropertyNames(collection: CollectionConfig): string[];
|
|
64
|
+
/** A `vector_search` naming no vector property. `vectorProperties` is {@link vectorPropertyNames}. */
|
|
65
|
+
export declare function unknownVectorProperty(property: string, vectorProperties: string[]): UnknownFieldAnswer;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { fieldKeyForColumn, findRelation, resolveCollectionRelations } from "../util/relations.es.js";
|
|
2
|
+
import { apiHiddenFieldNames } from "./field-access.es.js";
|
|
3
|
+
//#region src/collections/unknown-field.ts
|
|
4
|
+
/**
|
|
5
|
+
* The fields an unknown-field answer offers as valid, sorted: what a query on
|
|
6
|
+
* this collection can name as a column. A relation contributes its foreign key
|
|
7
|
+
* (`authorId`) when the key is on this table, and nothing otherwise. An
|
|
8
|
+
* `excludeFromApi` field is never offered.
|
|
9
|
+
*
|
|
10
|
+
* Read off the collection rather than the table, so the REST check — which
|
|
11
|
+
* holds no table — lists exactly what the driver lists.
|
|
12
|
+
*/
|
|
13
|
+
function queryableFieldNames(collection) {
|
|
14
|
+
const hidden = apiHiddenFieldNames(collection);
|
|
15
|
+
const names = /* @__PURE__ */ new Set();
|
|
16
|
+
let relations;
|
|
17
|
+
for (const [key, property] of Object.entries(collection.properties ?? {})) {
|
|
18
|
+
if (property?.type === "relation") {
|
|
19
|
+
relations ??= resolveCollectionRelations(collection);
|
|
20
|
+
const relation = findRelation(relations, key);
|
|
21
|
+
if (relation?.kind === "belongsTo") names.add(fieldKeyForColumn(collection, relation.localKey));
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
names.add(key);
|
|
25
|
+
}
|
|
26
|
+
return [...names].filter((name) => !hidden.has(name)).sort();
|
|
27
|
+
}
|
|
28
|
+
var validList = (validFields) => validFields.length > 0 ? `. Valid fields: ${validFields.join(", ")}` : "";
|
|
29
|
+
/** A filter key that resolves to nothing on `collectionPath`. */
|
|
30
|
+
function unknownFilterField(field, collectionPath, validFields) {
|
|
31
|
+
return {
|
|
32
|
+
message: `Unknown filter field '${field}' on collection '${collectionPath}'${validList(validFields)}`,
|
|
33
|
+
code: "UNKNOWN_FILTER_FIELD",
|
|
34
|
+
details: {
|
|
35
|
+
field,
|
|
36
|
+
collection: collectionPath,
|
|
37
|
+
...validFields.length > 0 && { validFields }
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** `author.emial` — a relation path whose last segment names nothing on the relation's target. */
|
|
42
|
+
function unknownRelationTargetField(path, target, collectionPath) {
|
|
43
|
+
return {
|
|
44
|
+
message: `Unknown field '${path.fieldKey}' on '${target.slug}', the target of relation '${path.relationKey}' on collection '${collectionPath}'${validList(target.validFields)}`,
|
|
45
|
+
code: "UNKNOWN_FILTER_FIELD",
|
|
46
|
+
details: {
|
|
47
|
+
field: path.field,
|
|
48
|
+
collection: collectionPath,
|
|
49
|
+
relation: path.relationKey,
|
|
50
|
+
targetCollection: target.slug,
|
|
51
|
+
...target.validFields.length > 0 && { validFields: target.validFields }
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** An `orderBy` key that resolves to nothing. `collectionName` is absent when the read carries no collection. */
|
|
56
|
+
function unknownOrderByField(orderBy, collectionName, validFields) {
|
|
57
|
+
return {
|
|
58
|
+
message: `Unknown orderBy field '${orderBy}'${collectionName ? ` on collection '${collectionName}'` : ""}${validList(validFields)}`,
|
|
59
|
+
code: "UNKNOWN_ORDER_BY_FIELD",
|
|
60
|
+
details: {
|
|
61
|
+
field: orderBy,
|
|
62
|
+
...collectionName && { collection: collectionName },
|
|
63
|
+
...validFields.length > 0 && { validFields }
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** A `fields` entry that names nothing. */
|
|
68
|
+
function unknownProjectionField(name, collectionName, validFields) {
|
|
69
|
+
return {
|
|
70
|
+
message: `Unknown field '${name}' in \`fields\` on collection '${collectionName}'. Valid fields: ${validFields.join(", ")}`,
|
|
71
|
+
code: "UNKNOWN_FIELD",
|
|
72
|
+
details: {
|
|
73
|
+
field: name,
|
|
74
|
+
collection: collectionName
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* An `include` naming no relation of the collection. `validRelations` is what
|
|
80
|
+
* the collection declares, never an `excludeFromApi` relation.
|
|
81
|
+
*/
|
|
82
|
+
function unknownIncludeRelation(key, collectionName, validRelations) {
|
|
83
|
+
return {
|
|
84
|
+
message: `Unknown relation '${key}' on collection '${collectionName}'` + (validRelations.length > 0 ? `. Its relations are: ${validRelations.join(", ")}` : ". It declares no relations."),
|
|
85
|
+
code: "UNKNOWN_RELATION",
|
|
86
|
+
details: {
|
|
87
|
+
relation: key,
|
|
88
|
+
collection: collectionName,
|
|
89
|
+
...validRelations.length > 0 && { validRelations }
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** The relations an unknown-relation answer offers, sorted — never an `excludeFromApi` one. */
|
|
94
|
+
function includableRelationNames(collection) {
|
|
95
|
+
const hidden = apiHiddenFieldNames(collection);
|
|
96
|
+
return Object.keys(resolveCollectionRelations(collection)).filter((key) => !hidden.has(key)).sort();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* An aggregate's `select` field or `groupBy` key that names no column.
|
|
100
|
+
* `forWhat` is where it was named: `sum()`, `count()`, `groupBy`.
|
|
101
|
+
* `validFields` is {@link queryableFieldNames}, never an `excludeFromApi` field.
|
|
102
|
+
*/
|
|
103
|
+
function unknownAggregateField(field, forWhat, validFields) {
|
|
104
|
+
return {
|
|
105
|
+
message: `Unknown field '${field}' in ${forWhat}${validList(validFields)}`,
|
|
106
|
+
code: "UNKNOWN_AGGREGATE_FIELD",
|
|
107
|
+
details: {
|
|
108
|
+
field,
|
|
109
|
+
...validFields.length > 0 && { validFields }
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The properties a `vector_search` can name, sorted: those declared
|
|
115
|
+
* `{ type: "vector" }`, never an `excludeFromApi` one.
|
|
116
|
+
*/
|
|
117
|
+
function vectorPropertyNames(collection) {
|
|
118
|
+
const hidden = apiHiddenFieldNames(collection);
|
|
119
|
+
return Object.entries(collection.properties ?? {}).filter(([key, property]) => property?.type === "vector" && !hidden.has(key)).map(([key]) => key).sort();
|
|
120
|
+
}
|
|
121
|
+
/** A `vector_search` naming no vector property. `vectorProperties` is {@link vectorPropertyNames}. */
|
|
122
|
+
function unknownVectorProperty(property, vectorProperties) {
|
|
123
|
+
return {
|
|
124
|
+
message: `Unknown vector property "${property}". ` + (vectorProperties.length > 0 ? `This collection's vector properties are: ${vectorProperties.join(", ")}.` : "This collection declares no `vector` property to search."),
|
|
125
|
+
code: "UNKNOWN_VECTOR_PROPERTY",
|
|
126
|
+
details: {
|
|
127
|
+
property,
|
|
128
|
+
...vectorProperties.length > 0 && { vectorProperties }
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
export { includableRelationNames, queryableFieldNames, unknownAggregateField, unknownFilterField, unknownIncludeRelation, unknownOrderByField, unknownProjectionField, unknownRelationTargetField, unknownVectorProperty, vectorPropertyNames };
|
|
134
|
+
|
|
135
|
+
//# sourceMappingURL=unknown-field.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unknown-field.es.js","names":[],"sources":["../../src/collections/unknown-field.ts"],"sourcesContent":["import type { CollectionConfig, Property } from \"@rebasepro/types\";\nimport { fieldKeyForColumn, findRelation, resolveCollectionRelations } from \"../util/relations\";\nimport { apiHiddenFieldNames } from \"./field-access\";\n\n/**\n * The answers a query gets for naming a field that does not exist — written\n * once, here, because two layers give them.\n *\n * The driver gives them for a typo. The REST check gives the same ones for a\n * field marked `excludeFromApi`: OpenAPI, `list_collections` and the generated\n * SDK all leave such a field out, so anything but the typo's answer — \"not\n * readable\", a different message, a list that names it — confirms it exists.\n * Both build the answer from these functions, so the two cannot drift apart by\n * a byte. Each returns the parts of the 400 (`message`, `code`, `details`) and\n * the caller wraps them in its own error type.\n *\n * @module\n */\n\nexport interface UnknownFieldAnswer {\n message: string;\n code: string;\n details: Record<string, unknown>;\n}\n\n/**\n * The fields an unknown-field answer offers as valid, sorted: what a query on\n * this collection can name as a column. A relation contributes its foreign key\n * (`authorId`) when the key is on this table, and nothing otherwise. An\n * `excludeFromApi` field is never offered.\n *\n * Read off the collection rather than the table, so the REST check — which\n * holds no table — lists exactly what the driver lists.\n */\nexport function queryableFieldNames(collection: CollectionConfig): string[] {\n const hidden = apiHiddenFieldNames(collection);\n const names = new Set<string>();\n let relations: ReturnType<typeof resolveCollectionRelations> | undefined;\n for (const [key, property] of Object.entries(collection.properties ?? {})) {\n if ((property as Property | undefined)?.type === \"relation\") {\n relations ??= resolveCollectionRelations(collection);\n const relation = findRelation(relations, key);\n if (relation?.kind === \"belongsTo\") names.add(fieldKeyForColumn(collection, relation.localKey));\n continue;\n }\n names.add(key);\n }\n return [...names].filter(name => !hidden.has(name)).sort();\n}\n\nconst validList = (validFields: string[]): string =>\n validFields.length > 0 ? `. Valid fields: ${validFields.join(\", \")}` : \"\";\n\n/** A filter key that resolves to nothing on `collectionPath`. */\nexport function unknownFilterField(field: string, collectionPath: string, validFields: string[]): UnknownFieldAnswer {\n return {\n message: `Unknown filter field '${field}' on collection '${collectionPath}'${validList(validFields)}`,\n code: \"UNKNOWN_FILTER_FIELD\",\n details: { field, collection: collectionPath, ...(validFields.length > 0 && { validFields }) }\n };\n}\n\n/** `author.emial` — a relation path whose last segment names nothing on the relation's target. */\nexport function unknownRelationTargetField(\n path: { field: string; relationKey: string; fieldKey: string },\n target: { slug: string; validFields: string[] },\n collectionPath: string\n): UnknownFieldAnswer {\n return {\n message: `Unknown field '${path.fieldKey}' on '${target.slug}', the target of relation ` +\n `'${path.relationKey}' on collection '${collectionPath}'${validList(target.validFields)}`,\n code: \"UNKNOWN_FILTER_FIELD\",\n details: {\n field: path.field,\n collection: collectionPath,\n relation: path.relationKey,\n targetCollection: target.slug,\n ...(target.validFields.length > 0 && { validFields: target.validFields })\n }\n };\n}\n\n/** An `orderBy` key that resolves to nothing. `collectionName` is absent when the read carries no collection. */\nexport function unknownOrderByField(orderBy: string, collectionName: string | undefined, validFields: string[]): UnknownFieldAnswer {\n const onCollection = collectionName ? ` on collection '${collectionName}'` : \"\";\n return {\n message: `Unknown orderBy field '${orderBy}'${onCollection}${validList(validFields)}`,\n code: \"UNKNOWN_ORDER_BY_FIELD\",\n details: {\n field: orderBy,\n ...(collectionName && { collection: collectionName }),\n ...(validFields.length > 0 && { validFields })\n }\n };\n}\n\n/** A `fields` entry that names nothing. */\nexport function unknownProjectionField(name: string, collectionName: string, validFields: string[]): UnknownFieldAnswer {\n return {\n message: `Unknown field '${name}' in \\`fields\\` on collection '${collectionName}'. Valid fields: ${validFields.join(\", \")}`,\n code: \"UNKNOWN_FIELD\",\n details: { field: name, collection: collectionName }\n };\n}\n\n/**\n * An `include` naming no relation of the collection. `validRelations` is what\n * the collection declares, never an `excludeFromApi` relation.\n */\nexport function unknownIncludeRelation(key: string, collectionName: string, validRelations: string[]): UnknownFieldAnswer {\n return {\n message: `Unknown relation '${key}' on collection '${collectionName}'`\n + (validRelations.length > 0\n ? `. Its relations are: ${validRelations.join(\", \")}`\n : \". It declares no relations.\"),\n code: \"UNKNOWN_RELATION\",\n details: { relation: key, collection: collectionName, ...(validRelations.length > 0 && { validRelations }) }\n };\n}\n\n/** The relations an unknown-relation answer offers, sorted — never an `excludeFromApi` one. */\nexport function includableRelationNames(collection: CollectionConfig): string[] {\n const hidden = apiHiddenFieldNames(collection);\n return Object.keys(resolveCollectionRelations(collection)).filter(key => !hidden.has(key)).sort();\n}\n\n/**\n * An aggregate's `select` field or `groupBy` key that names no column.\n * `forWhat` is where it was named: `sum()`, `count()`, `groupBy`.\n * `validFields` is {@link queryableFieldNames}, never an `excludeFromApi` field.\n */\nexport function unknownAggregateField(field: string, forWhat: string, validFields: string[]): UnknownFieldAnswer {\n return {\n message: `Unknown field '${field}' in ${forWhat}${validList(validFields)}`,\n code: \"UNKNOWN_AGGREGATE_FIELD\",\n details: { field, ...(validFields.length > 0 && { validFields }) }\n };\n}\n\n/**\n * The properties a `vector_search` can name, sorted: those declared\n * `{ type: \"vector\" }`, never an `excludeFromApi` one.\n */\nexport function vectorPropertyNames(collection: CollectionConfig): string[] {\n const hidden = apiHiddenFieldNames(collection);\n return Object.entries(collection.properties ?? {})\n .filter(([key, property]) => (property as Property | undefined)?.type === \"vector\" && !hidden.has(key))\n .map(([key]) => key)\n .sort();\n}\n\n/** A `vector_search` naming no vector property. `vectorProperties` is {@link vectorPropertyNames}. */\nexport function unknownVectorProperty(property: string, vectorProperties: string[]): UnknownFieldAnswer {\n return {\n message: `Unknown vector property \"${property}\". ` +\n (vectorProperties.length > 0\n ? `This collection's vector properties are: ${vectorProperties.join(\", \")}.`\n : \"This collection declares no `vector` property to search.\"),\n code: \"UNKNOWN_VECTOR_PROPERTY\",\n details: { property, ...(vectorProperties.length > 0 && { vectorProperties }) }\n };\n}\n"],"mappings":";;;;;;;;;;;;AAkCA,SAAgB,oBAAoB,YAAwC;CACxE,MAAM,SAAS,oBAAoB,UAAU;CAC7C,MAAM,wBAAQ,IAAI,IAAY;CAC9B,IAAI;CACJ,KAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,WAAW,cAAc,CAAC,CAAC,GAAG;EACvE,IAAK,UAAmC,SAAS,YAAY;GACzD,cAAc,2BAA2B,UAAU;GACnD,MAAM,WAAW,aAAa,WAAW,GAAG;GAC5C,IAAI,UAAU,SAAS,aAAa,MAAM,IAAI,kBAAkB,YAAY,SAAS,QAAQ,CAAC;GAC9F;EACJ;EACA,MAAM,IAAI,GAAG;CACjB;CACA,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,QAAO,SAAQ,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK;AAC7D;AAEA,IAAM,aAAa,gBACf,YAAY,SAAS,IAAI,mBAAmB,YAAY,KAAK,IAAI,MAAM;;AAG3E,SAAgB,mBAAmB,OAAe,gBAAwB,aAA2C;CACjH,OAAO;EACH,SAAS,yBAAyB,MAAM,mBAAmB,eAAe,GAAG,UAAU,WAAW;EAClG,MAAM;EACN,SAAS;GAAE;GAAO,YAAY;GAAgB,GAAI,YAAY,SAAS,KAAK,EAAE,YAAY;EAAG;CACjG;AACJ;;AAGA,SAAgB,2BACZ,MACA,QACA,gBACkB;CAClB,OAAO;EACH,SAAS,kBAAkB,KAAK,SAAS,QAAQ,OAAO,KAAK,6BACrD,KAAK,YAAY,mBAAmB,eAAe,GAAG,UAAU,OAAO,WAAW;EAC1F,MAAM;EACN,SAAS;GACL,OAAO,KAAK;GACZ,YAAY;GACZ,UAAU,KAAK;GACf,kBAAkB,OAAO;GACzB,GAAI,OAAO,YAAY,SAAS,KAAK,EAAE,aAAa,OAAO,YAAY;EAC3E;CACJ;AACJ;;AAGA,SAAgB,oBAAoB,SAAiB,gBAAoC,aAA2C;CAEhI,OAAO;EACH,SAAS,0BAA0B,QAAQ,GAF1B,iBAAiB,mBAAmB,eAAe,KAAK,KAEZ,UAAU,WAAW;EAClF,MAAM;EACN,SAAS;GACL,OAAO;GACP,GAAI,kBAAkB,EAAE,YAAY,eAAe;GACnD,GAAI,YAAY,SAAS,KAAK,EAAE,YAAY;EAChD;CACJ;AACJ;;AAGA,SAAgB,uBAAuB,MAAc,gBAAwB,aAA2C;CACpH,OAAO;EACH,SAAS,kBAAkB,KAAK,iCAAiC,eAAe,mBAAmB,YAAY,KAAK,IAAI;EACxH,MAAM;EACN,SAAS;GAAE,OAAO;GAAM,YAAY;EAAe;CACvD;AACJ;;;;;AAMA,SAAgB,uBAAuB,KAAa,gBAAwB,gBAA8C;CACtH,OAAO;EACH,SAAS,qBAAqB,IAAI,mBAAmB,eAAe,MAC7D,eAAe,SAAS,IACrB,wBAAwB,eAAe,KAAK,IAAI,MAChD;EACV,MAAM;EACN,SAAS;GAAE,UAAU;GAAK,YAAY;GAAgB,GAAI,eAAe,SAAS,KAAK,EAAE,eAAe;EAAG;CAC/G;AACJ;;AAGA,SAAgB,wBAAwB,YAAwC;CAC5E,MAAM,SAAS,oBAAoB,UAAU;CAC7C,OAAO,OAAO,KAAK,2BAA2B,UAAU,CAAC,CAAC,CAAC,QAAO,QAAO,CAAC,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK;AACpG;;;;;;AAOA,SAAgB,sBAAsB,OAAe,SAAiB,aAA2C;CAC7G,OAAO;EACH,SAAS,kBAAkB,MAAM,OAAO,UAAU,UAAU,WAAW;EACvE,MAAM;EACN,SAAS;GAAE;GAAO,GAAI,YAAY,SAAS,KAAK,EAAE,YAAY;EAAG;CACrE;AACJ;;;;;AAMA,SAAgB,oBAAoB,YAAwC;CACxE,MAAM,SAAS,oBAAoB,UAAU;CAC7C,OAAO,OAAO,QAAQ,WAAW,cAAc,CAAC,CAAC,CAAC,CAC7C,QAAQ,CAAC,KAAK,cAAe,UAAmC,SAAS,YAAY,CAAC,OAAO,IAAI,GAAG,CAAC,CAAC,CACtG,KAAK,CAAC,SAAS,GAAG,CAAC,CACnB,KAAK;AACd;;AAGA,SAAgB,sBAAsB,UAAkB,kBAAgD;CACpG,OAAO;EACH,SAAS,4BAA4B,SAAS,QACzC,iBAAiB,SAAS,IACrB,4CAA4C,iBAAiB,KAAK,IAAI,EAAE,KACxE;EACV,MAAM;EACN,SAAS;GAAE;GAAU,GAAI,iBAAiB,SAAS,KAAK,EAAE,iBAAiB;EAAG;CAClF;AACJ"}
|