@rebasepro/server 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/README.md +1 -1
- package/bin/rebase-server.js +4 -2
- package/dist/GCSStorageController-DivE9bAx.js +402 -0
- package/dist/GCSStorageController-DivE9bAx.js.map +1 -0
- package/dist/S3StorageController-D_vKs4kP.js +480 -0
- package/dist/S3StorageController-D_vKs4kP.js.map +1 -0
- package/dist/api/ast-schema-editor.d.ts +92 -1
- package/dist/api/errors.d.ts +9 -0
- package/dist/api/live-schema-routes.d.ts +38 -8
- package/dist/api/logs-routes.d.ts +39 -1
- package/dist/api/openapi-generator.d.ts +17 -0
- package/dist/api/rest/api-generator.d.ts +97 -11
- package/dist/api/rest/auth-collection-writes.d.ts +28 -21
- package/dist/api/rest/field-access-query.d.ts +28 -8
- package/dist/api/rest/field-ops.d.ts +19 -7
- package/dist/api/rest/query-body.d.ts +10 -0
- package/dist/api/rest/write-validation.d.ts +9 -7
- package/dist/api/schema-editor-routes.d.ts +10 -1
- package/dist/api/sql-export-routes.d.ts +8 -0
- package/dist/api/types.d.ts +28 -1
- package/dist/{ast-schema-editor-Mvr50v_S.js → ast-schema-editor-CSdL0iCk.js} +310 -12
- package/dist/ast-schema-editor-CSdL0iCk.js.map +1 -0
- package/dist/auth/access.d.ts +121 -0
- package/dist/auth/adapter-middleware.d.ts +2 -1
- package/dist/auth/address-ownership.d.ts +16 -1
- package/dist/auth/admin-roles-route.d.ts +4 -2
- package/dist/auth/admin-roles.d.ts +17 -20
- package/dist/auth/admin-user-ops.d.ts +24 -2
- package/dist/auth/admin-users-route.d.ts +2 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +56 -55
- package/dist/auth/api-keys/api-key-routes.d.ts +41 -11
- package/dist/auth/api-keys/api-key-store.d.ts +31 -8
- package/dist/auth/api-keys/api-key-types.d.ts +14 -16
- package/dist/auth/api-keys/http-operation.d.ts +19 -0
- package/dist/auth/api-keys/index.d.ts +11 -11
- package/dist/auth/api-keys/key-grant.d.ts +41 -0
- package/dist/auth/api-keys/legacy-permissions.d.ts +33 -0
- package/dist/auth/auth-hooks.d.ts +46 -7
- package/dist/auth/builtin-auth-adapter.d.ts +8 -0
- package/dist/auth/cookie-utils.d.ts +7 -0
- package/dist/auth/deliverable-address.d.ts +6 -0
- package/dist/auth/email-change-routes.d.ts +41 -0
- package/dist/auth/expired-token-sweep.d.ts +71 -0
- package/dist/auth/impersonation.d.ts +110 -0
- package/dist/auth/index.d.ts +4 -2
- package/dist/auth/interfaces.d.ts +120 -59
- package/dist/auth/jwt-crypto.d.ts +7 -0
- package/dist/auth/jwt-keys.d.ts +4 -0
- package/dist/auth/jwt.d.ts +54 -3
- package/dist/auth/magic-link-routes.d.ts +2 -6
- package/dist/auth/mfa-routes.d.ts +2 -9
- package/dist/auth/middleware.d.ts +17 -5
- package/dist/auth/otp-routes.d.ts +2 -6
- package/dist/auth/passwordless-signup.d.ts +27 -0
- package/dist/auth/platform-token.d.ts +122 -0
- package/dist/auth/rate-limiter.d.ts +54 -1
- package/dist/auth/registration-policy.d.ts +17 -0
- package/dist/auth/routes.d.ts +45 -0
- package/dist/auth/scope-routes.d.ts +22 -0
- package/dist/auth/session-routes.d.ts +11 -6
- package/dist/auth/token-revocation.d.ts +50 -1
- package/dist/auth/user-picking.d.ts +35 -0
- package/dist/auth/verify-credential.d.ts +28 -0
- package/dist/{auth-B-GIMpDG.js → auth-OPq_FeMs.js} +3115 -638
- package/dist/auth-OPq_FeMs.js.map +1 -0
- package/dist/auth_adapter-CIItCTc8.js +21 -0
- package/dist/auth_adapter-CIItCTc8.js.map +1 -0
- package/dist/backend-DTAOsLQc.js.map +1 -1
- package/dist/backup/backup-common.d.ts +24 -3
- package/dist/backup/backup-routes.d.ts +46 -5
- package/dist/backup/backup-schedule.d.ts +33 -0
- package/dist/backup/backup-storage.d.ts +16 -0
- package/dist/backup/index.d.ts +5 -1
- package/dist/backup-5d7XhLX3.js +581 -0
- package/dist/backup-5d7XhLX3.js.map +1 -0
- package/dist/boot/bundle.d.ts +19 -0
- package/dist/boot/driver.d.ts +3 -0
- package/dist/boot/env.d.ts +56 -4
- package/dist/boot/http-server.d.ts +22 -0
- package/dist/boot/options.d.ts +16 -1
- package/dist/boot/runtime-cors.d.ts +17 -0
- package/dist/boot/security-headers.d.ts +26 -0
- package/dist/boot/static-routing.d.ts +65 -0
- package/dist/collection_patch-BRu-BvDv.js +472 -0
- package/dist/collection_patch-BRu-BvDv.js.map +1 -0
- package/dist/{contract-routes-CbFjuBwa.js → contract-routes-02lASyJI.js} +5 -8
- package/dist/contract-routes-02lASyJI.js.map +1 -0
- package/dist/cron/cron-routes.d.ts +1 -1
- package/dist/cron/cron-scheduler.d.ts +94 -23
- package/dist/cron/cron-store.d.ts +31 -4
- package/dist/cron/define-cron.d.ts +6 -2
- package/dist/cron/index.d.ts +1 -1
- package/dist/{cron-loader-DfTj2Hbi.js → cron-loader-D7OmleAb.js} +11 -7
- package/dist/cron-loader-D7OmleAb.js.map +1 -0
- package/dist/{cron-routes-eE8nif_b.js → cron-routes-CBfiaqEL.js} +34 -8
- package/dist/cron-routes-CBfiaqEL.js.map +1 -0
- package/dist/{cron-scheduler-B0pLfAix.js → cron-scheduler-C2vbETMo.js} +349 -65
- package/dist/cron-scheduler-C2vbETMo.js.map +1 -0
- package/dist/cron-store-B2Ih0gCN.js +540 -0
- package/dist/cron-store-B2Ih0gCN.js.map +1 -0
- package/dist/{ddl-bootstrap-C6mo0Kmz.js → ddl-bootstrap-BaqMSa4Y.js} +2 -2
- package/dist/{ddl-bootstrap-C6mo0Kmz.js.map → ddl-bootstrap-BaqMSa4Y.js.map} +1 -1
- package/dist/email/index.d.ts +2 -2
- package/dist/email/smtp-email-service.d.ts +23 -0
- package/dist/email/templates.d.ts +22 -0
- package/dist/email/types.d.ts +44 -0
- package/dist/env.d.ts +1 -39
- package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
- package/dist/errors-D6_y86c5.js.map +1 -0
- package/dist/filter-dialect-REl-TjZr.js +1628 -0
- package/dist/filter-dialect-REl-TjZr.js.map +1 -0
- package/dist/{function-loader-xnbDAPfa.js → function-loader-BDKKiBmy.js} +5 -4
- package/dist/function-loader-BDKKiBmy.js.map +1 -0
- package/dist/{function-routes-Chet4-lB.js → function-routes-DWB7BBsi.js} +25 -13
- package/dist/function-routes-DWB7BBsi.js.map +1 -0
- package/dist/functions/context.d.ts +26 -6
- package/dist/functions/define-function.d.ts +7 -4
- package/dist/functions/guards.d.ts +22 -5
- package/dist/functions/index.d.ts +2 -2
- package/dist/functions/index.js +114 -37
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/request-timeout.d.ts +7 -5
- package/dist/history/history-routes.d.ts +9 -0
- package/dist/history/retention-sweep.d.ts +81 -0
- package/dist/{history-recorder-B4MpJfJK.js → history-recorder-DHenr3lm.js} +4 -4
- package/dist/{history-recorder-B4MpJfJK.js.map → history-recorder-DHenr3lm.js.map} +1 -1
- package/dist/{history-store-BhxWOuz9.js → history-store-DPofGVDp.js} +2 -2
- package/dist/{history-store-BhxWOuz9.js.map → history-store-DPofGVDp.js.map} +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.es.js +23363 -17552
- package/dist/index.es.js.map +1 -1
- package/dist/init/health.d.ts +17 -2
- package/dist/init/shutdown.d.ts +20 -0
- package/dist/init.d.ts +70 -6
- package/dist/internal-tables-L2t39RdT.js +136 -0
- package/dist/internal-tables-L2t39RdT.js.map +1 -0
- package/dist/jobs/job-queue.d.ts +8 -0
- package/dist/jobs/job-store.d.ts +24 -0
- package/dist/jobs/types.d.ts +25 -4
- package/dist/{jobs-CazMYhyy.js → jobs-BpD4Q0qR.js} +235 -25
- package/dist/jobs-BpD4Q0qR.js.map +1 -0
- package/dist/{jwt-DnQHNFCl.js → jwt-Bqcu7cGt.js} +207 -77
- package/dist/jwt-Bqcu7cGt.js.map +1 -0
- package/dist/{keys-CogCQpxG.js → keys-GAVZqbqx.js} +3 -17
- package/dist/{keys-CogCQpxG.js.map → keys-GAVZqbqx.js.map} +1 -1
- package/dist/load-failure-UvzZgGXT.js +48 -0
- package/dist/load-failure-UvzZgGXT.js.map +1 -0
- package/dist/{logger-DO2PZc4i.js → logger-D-S-hO5e.js} +26 -3
- package/dist/logger-D-S-hO5e.js.map +1 -0
- package/dist/{logs-routes-Bj4TYYUl.js → logs-routes-DAdv37GI.js} +48 -8
- package/dist/logs-routes-DAdv37GI.js.map +1 -0
- package/dist/mcp/consent-page.d.ts +1 -1
- package/dist/mcp/mcp-routes.d.ts +7 -0
- package/dist/mcp/mcp-tools.d.ts +23 -9
- package/dist/mcp/oauth-metadata.d.ts +21 -16
- package/dist/mcp/oauth-routes.d.ts +7 -1
- package/dist/metrics/index.d.ts +18 -0
- package/dist/metrics/pool-wait.d.ts +50 -0
- package/dist/offline-8Ks3yp9n-CadBKm2D.js +3305 -0
- package/dist/offline-8Ks3yp9n-CadBKm2D.js.map +1 -0
- package/dist/offline-store-D9ytNNaa-BCr2vsFB.js +1903 -0
- package/dist/offline-store-D9ytNNaa-BCr2vsFB.js.map +1 -0
- package/dist/{openapi-generator-O_O24MAT.js → openapi-generator-BqoUh4Ij.js} +455 -80
- package/dist/openapi-generator-BqoUh4Ij.js.map +1 -0
- package/dist/policy-PdzX-Opd.js +121 -0
- package/dist/policy-PdzX-Opd.js.map +1 -0
- package/dist/project_manifest-R6y_KHms.js +59 -0
- package/dist/project_manifest-R6y_KHms.js.map +1 -0
- package/dist/{proxy-Czngl3p9.js → proxy-qRlqeUmO.js} +2 -2
- package/dist/{proxy-Czngl3p9.js.map → proxy-qRlqeUmO.js.map} +1 -1
- package/dist/{query-parser-DGRVFNM3.js → query-parser-DK9eMkb9.js} +497 -52
- package/dist/query-parser-DK9eMkb9.js.map +1 -0
- package/dist/{request-timeout-C_4C2BeR.js → request-timeout-Dnl2_OBc.js} +17 -10
- package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
- package/dist/schema-edit/apply-schema-change.d.ts +73 -3
- package/dist/schema-edit/local-source.d.ts +29 -0
- package/dist/schema-edit/project-root.d.ts +3 -2
- package/dist/schema-edit/remote-source.d.ts +20 -5
- package/dist/schema-edit/source-editor.d.ts +28 -0
- package/dist/schema-editor-routes-BFf3i8jm.js +136 -0
- package/dist/schema-editor-routes-BFf3i8jm.js.map +1 -0
- package/dist/serve-spa.d.ts +58 -0
- package/dist/services/outbound-post.d.ts +50 -0
- package/dist/services/outbound-url-guard.d.ts +34 -5
- package/dist/services/routed-realtime-service.d.ts +16 -0
- package/dist/services/webhook-service.d.ts +66 -14
- package/dist/soft-delete-params-47yBGs3_.js +668 -0
- package/dist/soft-delete-params-47yBGs3_.js.map +1 -0
- package/dist/sql-export-routes-BNNuGGLP.js +240 -0
- package/dist/sql-export-routes-BNNuGGLP.js.map +1 -0
- package/dist/{src-vkcwKXbT.js → src-CatHFUym.js} +439 -20
- package/dist/src-CatHFUym.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +48 -2
- package/dist/storage/LocalStorageController.d.ts +69 -2
- package/dist/storage/S3StorageController.d.ts +39 -3
- package/dist/storage/image-transform.d.ts +5 -0
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/maintenance-routes.d.ts +23 -0
- package/dist/storage/maintenance.d.ts +126 -0
- package/dist/storage/property-limits.d.ts +41 -6
- package/dist/storage/rendition-cache.d.ts +9 -11
- package/dist/storage/request-keys.d.ts +15 -0
- package/dist/storage/request-principals.d.ts +30 -0
- package/dist/storage/requested-object.d.ts +74 -0
- package/dist/storage/routes.d.ts +52 -14
- package/dist/storage/source-variables.d.ts +10 -0
- package/dist/storage/streams.d.ts +42 -0
- package/dist/storage/tus-handler.d.ts +76 -7
- package/dist/storage/types.d.ts +101 -7
- package/dist/storage/upload-validation.d.ts +13 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
- package/dist/strings.es-BeZWAodn.js +256 -0
- package/dist/strings.es-BeZWAodn.js.map +1 -0
- package/dist/upload-validation-Dd-fnsbA.js +172 -0
- package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
- package/dist/utils/byte-size.d.ts +10 -0
- package/dist/utils/compression.d.ts +6 -0
- package/dist/utils/load-failure.d.ts +14 -0
- package/dist/utils/logger.d.ts +12 -0
- package/package.json +5 -5
- package/dist/GCSStorageController-CjrA4PMo.js +0 -242
- package/dist/GCSStorageController-CjrA4PMo.js.map +0 -1
- package/dist/S3StorageController-B6pKDNVj.js +0 -264
- package/dist/S3StorageController-B6pKDNVj.js.map +0 -1
- package/dist/admin-roles-vYdp_Pil.js +0 -36
- package/dist/admin-roles-vYdp_Pil.js.map +0 -1
- package/dist/admin_block-DxKLmdiv.js +0 -206
- package/dist/admin_block-DxKLmdiv.js.map +0 -1
- package/dist/ast-schema-editor-Mvr50v_S.js.map +0 -1
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
- package/dist/auth-B-GIMpDG.js.map +0 -1
- package/dist/backup-D7YR94N3.js +0 -253
- package/dist/backup-D7YR94N3.js.map +0 -1
- package/dist/contract-routes-CbFjuBwa.js.map +0 -1
- package/dist/cron-loader-DfTj2Hbi.js.map +0 -1
- package/dist/cron-routes-eE8nif_b.js.map +0 -1
- package/dist/cron-scheduler-B0pLfAix.js.map +0 -1
- package/dist/cron-store-TcoGz-xS.js +0 -305
- package/dist/cron-store-TcoGz-xS.js.map +0 -1
- package/dist/dynamic-import-X40pTZUQ.js +0 -23
- package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
- package/dist/errors-DWsX4yTd.js.map +0 -1
- package/dist/function-loader-xnbDAPfa.js.map +0 -1
- package/dist/function-routes-Chet4-lB.js.map +0 -1
- package/dist/jobs-CazMYhyy.js.map +0 -1
- package/dist/jwt-DnQHNFCl.js.map +0 -1
- package/dist/logger-DO2PZc4i.js.map +0 -1
- package/dist/logs-routes-Bj4TYYUl.js.map +0 -1
- package/dist/openapi-generator-O_O24MAT.js.map +0 -1
- package/dist/query-parser-DGRVFNM3.js.map +0 -1
- package/dist/request-timeout-C_4C2BeR.js.map +0 -1
- package/dist/schema-editor-routes-C5-lh_jO.js +0 -87
- package/dist/schema-editor-routes-C5-lh_jO.js.map +0 -1
- package/dist/src-pmvW7BFx.js +0 -6688
- package/dist/src-pmvW7BFx.js.map +0 -1
- package/dist/src-vkcwKXbT.js.map +0 -1
- package/dist/types-BfKcm9do.js +0 -39
- package/dist/types-BfKcm9do.js.map +0 -1
|
@@ -2,9 +2,207 @@ import { createRequire as __rebaseCreateRequire } from "module";
|
|
|
2
2
|
import __rebaseProcess from "process";
|
|
3
3
|
globalThis.process ??= __rebaseProcess;
|
|
4
4
|
__rebaseCreateRequire(import.meta.url);
|
|
5
|
-
import {
|
|
6
|
-
import "./
|
|
5
|
+
import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
|
|
6
|
+
import "./strings.es-BeZWAodn.js";
|
|
7
|
+
import "./src-CatHFUym.js";
|
|
8
|
+
import { r as policy } from "./policy-PdzX-Opd.js";
|
|
9
|
+
import { c as fieldKeyForColumn, f as isRelationRequired, l as findRelation, m as resolveCollectionRelations, n as HARD_DELETE_QUERY_PARAM, o as effectiveAccess, t as DELETED_QUERY_PARAM, u as getTableName } from "./soft-delete-params-47yBGs3_.js";
|
|
10
|
+
//#region ../types/src/types/relations.ts
|
|
11
|
+
/**
|
|
12
|
+
* The key a junction row's own columns are carried under, in both directions.
|
|
13
|
+
*
|
|
14
|
+
* A read that includes a `manyToMany` relation serves each related row with its
|
|
15
|
+
* link's columns nested here — `{ id: 5, name: "ts", _pivot: { role: "owner" } }`
|
|
16
|
+
* — and a membership write may name the same key on an element to state what
|
|
17
|
+
* the link should hold. One constant because the two have to be the same word:
|
|
18
|
+
* a wire name that differs between the read and the write it round-trips
|
|
19
|
+
* through is a shape no client can echo back.
|
|
20
|
+
*
|
|
21
|
+
* Leading underscore, like `_matches`: it reads as metadata about the row
|
|
22
|
+
* rather than as one of its columns. A payload property may not be named
|
|
23
|
+
* `_pivot` either — `checkJunctionPayload` refuses it — so the key means one
|
|
24
|
+
* thing wherever it appears.
|
|
25
|
+
*
|
|
26
|
+
* @group Models
|
|
27
|
+
*/
|
|
28
|
+
var JUNCTION_PIVOT_KEY = "_pivot";
|
|
29
|
+
/**
|
|
30
|
+
* The key a row carries the totals of its counted relations under.
|
|
31
|
+
*
|
|
32
|
+
* An include that asks for a count — `include: { tags: { limit: 4, count: true } }`
|
|
33
|
+
* — serves, beside the (capped) `tags`, how many related rows there are in all:
|
|
34
|
+
* `{ id: 1, tags: [ …4 rows ], _count: { tags: 37 } }`. One object per row,
|
|
35
|
+
* keyed by relation name, so counting a second relation adds a key rather than
|
|
36
|
+
* a second top-level name.
|
|
37
|
+
*
|
|
38
|
+
* Leading underscore, like {@link JUNCTION_PIVOT_KEY} and `_matches`: it
|
|
39
|
+
* describes the read, not the record, and no column or form writes it. A
|
|
40
|
+
* collection with a property of this name cannot ask for a count — the read
|
|
41
|
+
* answers 400 rather than writing over the column.
|
|
42
|
+
*
|
|
43
|
+
* @group Models
|
|
44
|
+
*/
|
|
45
|
+
var INCLUDE_COUNT_KEY = "_count";
|
|
46
|
+
/** @group Models */
|
|
47
|
+
function hasForeignKeyOnTarget(relation) {
|
|
48
|
+
return relation.kind === "hasOne" || relation.kind === "hasMany";
|
|
49
|
+
}
|
|
50
|
+
/** @group Models */
|
|
51
|
+
function isManyToMany(relation) {
|
|
52
|
+
return relation.kind === "manyToMany";
|
|
53
|
+
}
|
|
54
|
+
/** @group Models */
|
|
55
|
+
function isToMany(relation) {
|
|
56
|
+
return relation.cardinality === "many";
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region ../types/src/types/tenancy.ts
|
|
60
|
+
/** Narrow a {@link TenantSource} to its claim form. @group Models */
|
|
61
|
+
function isTenantClaimSource(source) {
|
|
62
|
+
return typeof source.claim === "string";
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The roles tenancy does not apply to, when the collection names none.
|
|
66
|
+
*
|
|
67
|
+
* `admin`, mirroring the security baseline every collection already carries
|
|
68
|
+
* (`<table>_default_admin_read` / `_write`): the Studio, `dataAsAdmin` and a
|
|
69
|
+
* support operator all run with it, and a tenancy rule that locked them out
|
|
70
|
+
* would make the admin panel show an empty table on a collection full of rows.
|
|
71
|
+
*
|
|
72
|
+
* @group Models
|
|
73
|
+
*/
|
|
74
|
+
var DEFAULT_TENANT_BYPASS_ROLES = ["admin"];
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region ../common/src/util/tenant.ts
|
|
77
|
+
/**
|
|
78
|
+
* The one reading of `collection.tenant`.
|
|
79
|
+
*
|
|
80
|
+
* Four things have to agree for a tenant-scoped collection to work — the
|
|
81
|
+
* column, the RLS policy, the value stamped on insert and the index — and
|
|
82
|
+
* before this they were four hand-written declarations that nothing compared.
|
|
83
|
+
* The three that are schema become a {@link SecurityRule} and a column effect
|
|
84
|
+
* derived here and in `planSchema`; the fourth, the write path, is
|
|
85
|
+
* {@link resolveTenantWrite}.
|
|
86
|
+
*
|
|
87
|
+
* Everything in this module is pure. The policy it builds is a `SecurityRule`
|
|
88
|
+
* like any other, which is what makes `db push`, the doctor, boot-ensure, the
|
|
89
|
+
* drift detector and the Studio treat the tenancy policy as what it is —
|
|
90
|
+
* generated, named, and recognisable — rather than as somebody's hand-written
|
|
91
|
+
* SQL that a push should offer to drop.
|
|
92
|
+
*/
|
|
93
|
+
/**
|
|
94
|
+
* The collection's tenancy declaration, or nothing.
|
|
95
|
+
*
|
|
96
|
+
* Read through this rather than off the object, so the one shape check —
|
|
97
|
+
* `tenant` is an object carrying a `field` and a `from` — is in one place. A
|
|
98
|
+
* config that is *wrong* is refused by `validateCollectionConfig` with a
|
|
99
|
+
* message; this is only asking whether there is one.
|
|
100
|
+
*/
|
|
101
|
+
function getTenantConfig(collection) {
|
|
102
|
+
const tenant = collection?.tenant;
|
|
103
|
+
if (!tenant || typeof tenant !== "object") return void 0;
|
|
104
|
+
if (typeof tenant.field !== "string" || !tenant.field) return void 0;
|
|
105
|
+
if (!tenant.from || typeof tenant.from !== "object") return void 0;
|
|
106
|
+
return tenant;
|
|
107
|
+
}
|
|
108
|
+
/** The roles tenancy does not apply to, defaulted. */
|
|
109
|
+
function tenantBypassRoles(tenant) {
|
|
110
|
+
return tenant.bypassRoles ?? DEFAULT_TENANT_BYPASS_ROLES;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The name of the policy a tenant declaration compiles to.
|
|
114
|
+
*
|
|
115
|
+
* Explicit — not a `getPolicyNameHash` of the rule — precisely because the
|
|
116
|
+
* rule's *body* is compiled with more information in some callers than in
|
|
117
|
+
* others (`planSchema` can resolve a relation's target collection and so knows
|
|
118
|
+
* the column's type; the Studio, asking only for names, cannot). A hashed name
|
|
119
|
+
* would then differ between the two, and the same policy would read as drift.
|
|
120
|
+
* A frozen identifier: see `contracts/derived-names.txt`.
|
|
121
|
+
*/
|
|
122
|
+
function tenantPolicyName(tableName) {
|
|
123
|
+
return `${tableName}_tenant_scope`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The condition a tenant declaration means, as a policy expression.
|
|
127
|
+
*
|
|
128
|
+
* `serverContext()` first, for the same reason every injected baseline rule
|
|
129
|
+
* carries it: the trusted plane runs migrations, the auth flows and the boot,
|
|
130
|
+
* and a restrictive policy that excluded it would not protect a tenant, it
|
|
131
|
+
* would stop the server from starting.
|
|
132
|
+
*
|
|
133
|
+
* Then the bypass roles, then the tenancy test itself — a claim comparison or a
|
|
134
|
+
* correlated `EXISTS` over the membership table, which are the two ways a
|
|
135
|
+
* deployment answers "which tenant is this caller in".
|
|
136
|
+
*/
|
|
137
|
+
function tenantScopeExpression(tenant) {
|
|
138
|
+
const match = isTenantClaimSource(tenant.from) ? policy.compare(policy.field(tenant.field), "eq", policy.authClaim(tenant.from.claim)) : policy.existsIn({
|
|
139
|
+
collection: tenant.from.membership.collection,
|
|
140
|
+
where: policy.and(policy.compare(policy.field(tenant.from.membership.tenantField), "eq", policy.outerField(tenant.field)), policy.compare(policy.field(tenant.from.membership.userField), "eq", policy.authUid()))
|
|
141
|
+
});
|
|
142
|
+
const bypass = tenantBypassRoles(tenant);
|
|
143
|
+
return bypass.length > 0 ? policy.or(policy.serverContext(), policy.rolesOverlap(bypass), match) : policy.or(policy.serverContext(), match);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The rule a tenant declaration compiles to, or nothing when there is none.
|
|
147
|
+
*
|
|
148
|
+
* **Restrictive**, and that is the whole design. A restrictive policy is ANDed
|
|
149
|
+
* with every other policy on the table, so tenancy narrows what the
|
|
150
|
+
* collection's own `securityRules` allow and can never widen it. A permissive
|
|
151
|
+
* one would OR with them, and a single `access: "public"` rule elsewhere in the
|
|
152
|
+
* file would take the entire tenancy boundary off without contradicting
|
|
153
|
+
* anything a reader could see.
|
|
154
|
+
*
|
|
155
|
+
* One rule with `operation: "all"` rather than four with `operations: [...]`:
|
|
156
|
+
* `FOR ALL` gives Postgres the USING clause for SELECT/UPDATE/DELETE and the
|
|
157
|
+
* WITH CHECK clause for INSERT/UPDATE, which is exactly the coverage wanted,
|
|
158
|
+
* as one policy with one name instead of four.
|
|
159
|
+
*/
|
|
160
|
+
function buildTenantSecurityRule(collection) {
|
|
161
|
+
const tenant = getTenantConfig(collection);
|
|
162
|
+
if (!tenant) return void 0;
|
|
163
|
+
const expression = tenantScopeExpression(tenant);
|
|
164
|
+
return {
|
|
165
|
+
name: tenantPolicyName(getTableName(collection)),
|
|
166
|
+
mode: "restrictive",
|
|
167
|
+
operation: "all",
|
|
168
|
+
condition: expression,
|
|
169
|
+
check: expression
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The property a `softDelete` collection records a deletion in — its wire
|
|
174
|
+
* name, what a delete stamps and what a restore (an update to `null`) clears —
|
|
175
|
+
* or `undefined` for a collection without soft delete.
|
|
176
|
+
*
|
|
177
|
+
* One reading of the declaration for every layer that needs it: the driver
|
|
178
|
+
* that stamps and filters on it, and the REST route that has to recognise a
|
|
179
|
+
* restore before it looks the row up.
|
|
180
|
+
*/
|
|
181
|
+
function softDeleteFieldOf(collection) {
|
|
182
|
+
const declared = collection && "softDelete" in collection ? collection.softDelete : void 0;
|
|
183
|
+
if (!declared) return void 0;
|
|
184
|
+
return typeof declared === "object" && declared.field || "deletedAt";
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Whether an update's values restore a soft-deleted row: they set the
|
|
188
|
+
* soft-delete field back to `null`, the documented restore.
|
|
189
|
+
*
|
|
190
|
+
* A restore is an ordinary update of a row every default read hides, so a door
|
|
191
|
+
* that looks the row up before updating it has to include the trashed rows for
|
|
192
|
+
* this update — and only for this one: any other edit of a trashed row is a
|
|
193
|
+
* 404.
|
|
194
|
+
*/
|
|
195
|
+
function restoresSoftDeletedRow(collection, values) {
|
|
196
|
+
const field = softDeleteFieldOf(collection);
|
|
197
|
+
return field !== void 0 && values?.[field] === null;
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
7
200
|
//#region src/api/openapi-generator.ts
|
|
201
|
+
var openapi_generator_exports = /* @__PURE__ */ __exportAll({
|
|
202
|
+
buildCollectionInputSchema: () => buildCollectionInputSchema,
|
|
203
|
+
buildCollectionSchema: () => buildCollectionSchema,
|
|
204
|
+
generateOpenApiSpec: () => generateOpenApiSpec
|
|
205
|
+
});
|
|
8
206
|
function generateOpenApiSpec(collections, options = {}) {
|
|
9
207
|
const basePath = options.basePath ?? "/api";
|
|
10
208
|
const requireAuth = options.requireAuth ?? true;
|
|
@@ -47,7 +245,7 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
47
245
|
type: "integer",
|
|
48
246
|
minimum: 1
|
|
49
247
|
},
|
|
50
|
-
description: "Page number (alternative to offset). Calculates offset as (page-1)*limit"
|
|
248
|
+
description: "Page number (alternative to offset). Calculates offset as (page-1)*limit. Cannot be combined with `offset` (400 PAGE_WITH_OFFSET)."
|
|
51
249
|
},
|
|
52
250
|
{
|
|
53
251
|
name: "after",
|
|
@@ -108,9 +306,20 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
108
306
|
name: "distinct",
|
|
109
307
|
in: "query",
|
|
110
308
|
schema: { type: "boolean" },
|
|
111
|
-
description: "`SELECT DISTINCT` over the returned columns. Only meaningful alongside `fields`: the primary key is always in the projection, so without narrowing it every row is already distinct. The response has no `meta.total` — the rows before deduplication are a different set, so their count would not describe the page — and `meta.hasMore`
|
|
309
|
+
description: "`SELECT DISTINCT` over the returned columns. Only meaningful alongside `fields`: the primary key is always in the projection, so without narrowing it every row is already distinct. The response has no `meta.total` — the rows before deduplication are a different set, so their count would not describe the page — and `meta.hasMore` comes from reading one row past the page. Refused (400) alongside `searchString` or a vector search, which attach a per-row score that makes every row distinct by construction, and (400 DISTINCT_ORDER_BY_NOT_SELECTED) when `orderBy` names a column `fields` does not return.",
|
|
112
310
|
example: "true"
|
|
113
311
|
},
|
|
312
|
+
{
|
|
313
|
+
name: "count",
|
|
314
|
+
in: "query",
|
|
315
|
+
schema: {
|
|
316
|
+
type: "string",
|
|
317
|
+
enum: ["exact", "none"],
|
|
318
|
+
default: "exact"
|
|
319
|
+
},
|
|
320
|
+
description: "Whether to count every matching row for `meta.total`. `exact` (the default) runs a `COUNT(*)` beside the page, which on a large table costs more than the page itself; `none` skips it, and the response then has no `meta.total`. `meta.hasMore` is exact either way — without a count it comes from reading one row past the page — so a walk that pages until `hasMore` is false needs no count. Anything else is a 400 INVALID_COUNT.",
|
|
321
|
+
example: "none"
|
|
322
|
+
},
|
|
114
323
|
{
|
|
115
324
|
name: "searchString",
|
|
116
325
|
in: "query",
|
|
@@ -149,7 +358,7 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
149
358
|
name: "vector_threshold",
|
|
150
359
|
in: "query",
|
|
151
360
|
schema: { type: "number" },
|
|
152
|
-
description: "Drop rows farther than this distance. Rows are returned closest-first with a `_distance` field."
|
|
361
|
+
description: "Drop rows farther than this distance. Rows are returned closest-first with a `_distance` field. The response then has no `meta.total`: counting the rows within a distance computes it for every row of the table. `meta.hasMore` is exact, from reading one row past the page."
|
|
153
362
|
}
|
|
154
363
|
];
|
|
155
364
|
const spec = {
|
|
@@ -183,7 +392,7 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
183
392
|
properties: {
|
|
184
393
|
total: {
|
|
185
394
|
type: "integer",
|
|
186
|
-
description: "Total number of matching records. Absent on a `distinct` read, which has no count of the rows it returns
|
|
395
|
+
description: "Total number of matching records. Absent on a `distinct` read, which has no count of the rows it returns, on a vector search with a `vector_threshold`, whose count would compute a distance for every row, and when the request asked for `count=none`."
|
|
187
396
|
},
|
|
188
397
|
limit: {
|
|
189
398
|
type: "integer",
|
|
@@ -220,7 +429,50 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
220
429
|
const paths = spec.paths;
|
|
221
430
|
const schemas = spec.components.schemas;
|
|
222
431
|
const tags = spec.tags;
|
|
223
|
-
const reservedParameterNames = new Set(listQueryParameters().map((p) => p.name));
|
|
432
|
+
const reservedParameterNames = /* @__PURE__ */ new Set([...listQueryParameters().map((p) => p.name), DELETED_QUERY_PARAM]);
|
|
433
|
+
/**
|
|
434
|
+
* `?deleted=` on the reads of a `softDelete` collection. A delete there
|
|
435
|
+
* moves the row to the trash, and every read hides trashed rows unless it
|
|
436
|
+
* asks for them; the list, the single row, the count and the aggregate all
|
|
437
|
+
* honour this. Empty for any other collection.
|
|
438
|
+
*/
|
|
439
|
+
const deletedParameters = (collection) => {
|
|
440
|
+
const field = softDeleteFieldOf(collection);
|
|
441
|
+
if (!field) return [];
|
|
442
|
+
return [{
|
|
443
|
+
name: DELETED_QUERY_PARAM,
|
|
444
|
+
in: "query",
|
|
445
|
+
required: false,
|
|
446
|
+
schema: {
|
|
447
|
+
type: "string",
|
|
448
|
+
enum: ["include", "only"]
|
|
449
|
+
},
|
|
450
|
+
description: `Rows a delete moved to the trash (\`${field}\` set) are left out unless this asks for them: \`include\` reads them with the live rows, \`only\` reads them alone. Any other value is a 400 INVALID_DELETED_PARAM.`
|
|
451
|
+
}];
|
|
452
|
+
};
|
|
453
|
+
/**
|
|
454
|
+
* `?hard=` on the deletes of a `softDelete` collection: delete for good
|
|
455
|
+
* rather than move to the trash. Empty for any other collection.
|
|
456
|
+
*/
|
|
457
|
+
const hardDeleteParameters = (collection) => {
|
|
458
|
+
if (!softDeleteFieldOf(collection)) return [];
|
|
459
|
+
return [{
|
|
460
|
+
name: HARD_DELETE_QUERY_PARAM,
|
|
461
|
+
in: "query",
|
|
462
|
+
required: false,
|
|
463
|
+
schema: {
|
|
464
|
+
type: "boolean",
|
|
465
|
+
default: false
|
|
466
|
+
},
|
|
467
|
+
description: "`true` deletes for good instead of moving to the trash — a row already in the trash included, which is how the trash is emptied. Only `true`/`1` and `false`/`0` are accepted; anything else is a 400 INVALID_HARD_PARAM."
|
|
468
|
+
}];
|
|
469
|
+
};
|
|
470
|
+
/** What a delete does to a `softDelete` collection's row, for the delete operations' descriptions. */
|
|
471
|
+
const softDeleteBehaviour = (collection) => {
|
|
472
|
+
const field = softDeleteFieldOf(collection);
|
|
473
|
+
if (!field) return void 0;
|
|
474
|
+
return `This collection soft-deletes: a delete moves the row to the trash by stamping \`${field}\`, and every read leaves it out unless it passes \`?deleted=include\` or \`?deleted=only\`. A PATCH setting \`${field}\` back to \`null\` restores it. Pass \`?hard=true\` to delete for good.`;
|
|
475
|
+
};
|
|
224
476
|
const registeredSchemas = new Set((collections || []).map(schemaNameFor));
|
|
225
477
|
/**
|
|
226
478
|
* `Prefer: return=minimal`, on every route that would otherwise send a row
|
|
@@ -387,27 +639,59 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
387
639
|
schemas[`${schemaName}Input`] = buildCollectionInputSchema(collection);
|
|
388
640
|
schemas[`${schemaName}Update`] = buildCollectionUpdateSchema(collection);
|
|
389
641
|
const dataPath = `/data/${slug}`;
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
642
|
+
const softDeleteNote = softDeleteBehaviour(collection);
|
|
643
|
+
const countNarrowing = /* @__PURE__ */ new Set([
|
|
644
|
+
"where",
|
|
645
|
+
"or",
|
|
646
|
+
"and",
|
|
647
|
+
"not",
|
|
648
|
+
"searchString",
|
|
649
|
+
"vector_search",
|
|
650
|
+
"vector",
|
|
651
|
+
"vector_distance",
|
|
652
|
+
"vector_threshold"
|
|
653
|
+
]);
|
|
654
|
+
const countParameters = [
|
|
655
|
+
...listQueryParameters().filter((p) => countNarrowing.has(p.name)).map((p) => p.name === "vector_threshold" ? {
|
|
656
|
+
...p,
|
|
657
|
+
description: "Count only the rows within this distance of `vector`."
|
|
658
|
+
} : p),
|
|
659
|
+
...deletedParameters(collection),
|
|
660
|
+
...buildFilterParameters(collection, reservedParameterNames)
|
|
661
|
+
];
|
|
662
|
+
const countResponses = {
|
|
663
|
+
200: {
|
|
664
|
+
description: "The number of matching rows",
|
|
665
|
+
content: { "application/json": { schema: {
|
|
666
|
+
type: "object",
|
|
667
|
+
required: ["count"],
|
|
668
|
+
properties: { count: {
|
|
669
|
+
type: "integer",
|
|
670
|
+
description: "Rows matching the filters"
|
|
671
|
+
} }
|
|
672
|
+
} } }
|
|
673
|
+
},
|
|
674
|
+
...errorResponses(requireAuth)
|
|
675
|
+
};
|
|
676
|
+
paths[`${dataPath}/count`] = {
|
|
677
|
+
get: {
|
|
678
|
+
tags: [collection.name],
|
|
679
|
+
summary: `Count ${collection.name}`,
|
|
680
|
+
description: "The number of rows the same filters would return, without returning them. Takes the filter and search parameters of the list endpoint; `limit`, `offset` and `orderBy` are not part of the question and are ignored.",
|
|
681
|
+
operationId: `count${schemaName}`,
|
|
682
|
+
parameters: countParameters,
|
|
683
|
+
responses: countResponses
|
|
684
|
+
},
|
|
685
|
+
post: queryOperation({
|
|
686
|
+
tags: [collection.name],
|
|
687
|
+
summary: `Count ${collection.name}, parameters in the body`,
|
|
688
|
+
description: "The count `GET` answers, with its query parameters in a JSON body instead of the URL: the same filters, search and vector search, the same row-level security, and the same errors. It reads and never writes, so a credential needs `data:read` for it. Use it when the URL would not fit: a vector search's embedding, a long `in` list. A parameter also on the URL is a 400 INVALID_QUERY_BODY.",
|
|
689
|
+
operationId: `count${schemaName}Query`,
|
|
690
|
+
parameters: countParameters,
|
|
691
|
+
responses: countResponses,
|
|
692
|
+
reserved: reservedParameterNames
|
|
693
|
+
})
|
|
694
|
+
};
|
|
411
695
|
paths[`${dataPath}/aggregate`] = { get: {
|
|
412
696
|
tags: [collection.name],
|
|
413
697
|
summary: `Aggregate ${collection.name}`,
|
|
@@ -439,6 +723,7 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
439
723
|
schema: { type: "string" },
|
|
440
724
|
example: "count:desc"
|
|
441
725
|
},
|
|
726
|
+
...deletedParameters(collection),
|
|
442
727
|
...buildFilterParameters(collection, reservedParameterNames)
|
|
443
728
|
],
|
|
444
729
|
responses: {
|
|
@@ -474,28 +759,42 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
474
759
|
...errorResponses(requireAuth)
|
|
475
760
|
}
|
|
476
761
|
} };
|
|
762
|
+
const listParameters = [
|
|
763
|
+
...listQueryParameters(),
|
|
764
|
+
...deletedParameters(collection),
|
|
765
|
+
...buildFilterParameters(collection, reservedParameterNames)
|
|
766
|
+
];
|
|
767
|
+
const listResponses = {
|
|
768
|
+
200: {
|
|
769
|
+
description: "Paginated list of entities",
|
|
770
|
+
content: { "application/json": { schema: {
|
|
771
|
+
type: "object",
|
|
772
|
+
properties: {
|
|
773
|
+
data: {
|
|
774
|
+
type: "array",
|
|
775
|
+
items: { $ref: `#/components/schemas/${schemaName}` }
|
|
776
|
+
},
|
|
777
|
+
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
778
|
+
}
|
|
779
|
+
} } }
|
|
780
|
+
},
|
|
781
|
+
...errorResponses(requireAuth)
|
|
782
|
+
};
|
|
783
|
+
paths[`${dataPath}/query`] = { post: queryOperation({
|
|
784
|
+
tags: [collection.name],
|
|
785
|
+
summary: `List ${collection.name}, parameters in the body`,
|
|
786
|
+
operationId: `query${schemaName}`,
|
|
787
|
+
parameters: listParameters,
|
|
788
|
+
responses: listResponses,
|
|
789
|
+
reserved: reservedParameterNames
|
|
790
|
+
}) };
|
|
477
791
|
paths[dataPath] = {
|
|
478
792
|
get: {
|
|
479
793
|
tags: [collection.name],
|
|
480
794
|
summary: `List ${collection.name}`,
|
|
481
795
|
operationId: `list${schemaName}`,
|
|
482
|
-
parameters:
|
|
483
|
-
responses:
|
|
484
|
-
200: {
|
|
485
|
-
description: "Paginated list of entities",
|
|
486
|
-
content: { "application/json": { schema: {
|
|
487
|
-
type: "object",
|
|
488
|
-
properties: {
|
|
489
|
-
data: {
|
|
490
|
-
type: "array",
|
|
491
|
-
items: { $ref: `#/components/schemas/${schemaName}` }
|
|
492
|
-
},
|
|
493
|
-
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
494
|
-
}
|
|
495
|
-
} } }
|
|
496
|
-
},
|
|
497
|
-
...errorResponses(requireAuth)
|
|
498
|
-
}
|
|
796
|
+
parameters: listParameters,
|
|
797
|
+
responses: listResponses
|
|
499
798
|
},
|
|
500
799
|
post: {
|
|
501
800
|
tags: [collection.name],
|
|
@@ -506,8 +805,8 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
506
805
|
in: "query",
|
|
507
806
|
required: false,
|
|
508
807
|
schema: { type: "string" },
|
|
509
|
-
description: "Comma-separated columns to upsert on, turning the create into INSERT ... ON CONFLICT DO UPDATE. They must carry a declared uniqueness guarantee — `validation.unique`, a `unique` index, or the primary key — or the request is refused with a 400 naming the targets that do exist. Left off, this is a plain insert and a duplicate key still raises.",
|
|
510
|
-
example: "
|
|
808
|
+
description: "Comma-separated columns to upsert on, turning the create into INSERT ... ON CONFLICT DO UPDATE. They must carry a declared uniqueness guarantee — `validation.unique`, a `unique` index, or the primary key — or the request is refused with a 400 naming the targets that do exist. Left off, this is a plain insert and a duplicate key still raises. Refused (400 `INVALID_CONFLICT_TARGET`) on the auth collection, whose rows are the users: create them with a plain POST and change them with PATCH.",
|
|
809
|
+
example: "sku"
|
|
511
810
|
}, preferHeader],
|
|
512
811
|
requestBody: {
|
|
513
812
|
required: true,
|
|
@@ -564,7 +863,7 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
564
863
|
},
|
|
565
864
|
upsert: {
|
|
566
865
|
type: "boolean",
|
|
567
|
-
description: "Write each row as INSERT ... ON CONFLICT DO UPDATE."
|
|
866
|
+
description: "Write each row as INSERT ... ON CONFLICT DO UPDATE. Refused (400 `INVALID_CONFLICT_TARGET`) on the auth collection, whose rows are the users."
|
|
568
867
|
},
|
|
569
868
|
onConflict: {
|
|
570
869
|
type: "array",
|
|
@@ -649,9 +948,9 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
649
948
|
paths[`/data/${slug}/bulk/delete`] = { post: {
|
|
650
949
|
tags: [collection.name],
|
|
651
950
|
summary: `Delete many ${collection.name} in one transaction`,
|
|
652
|
-
description: "A POST, not `DELETE /bulk` with a body. Bodies on DELETE are permitted but widely dropped by proxies and CDNs, and several generators ignore `requestBody` on a DELETE operation — a generated client would send the request with no ids at all. Takes ids rather than a filter: a mistyped condition that empties a table cannot be reviewed at the call site the way an explicit list can. `beforeDelete`/`afterDelete` fire per row.",
|
|
951
|
+
description: "A POST, not `DELETE /bulk` with a body. Bodies on DELETE are permitted but widely dropped by proxies and CDNs, and several generators ignore `requestBody` on a DELETE operation — a generated client would send the request with no ids at all. Takes ids rather than a filter: a mistyped condition that empties a table cannot be reviewed at the call site the way an explicit list can. `beforeDelete`/`afterDelete` fire per row." + (softDeleteNote ? `\n\n${softDeleteNote}` : ""),
|
|
653
952
|
operationId: `deleteMany${schemaName}`,
|
|
654
|
-
parameters: [idempotencyHeader],
|
|
953
|
+
parameters: [idempotencyHeader, ...hardDeleteParameters(collection)],
|
|
655
954
|
requestBody: {
|
|
656
955
|
required: true,
|
|
657
956
|
content: { "application/json": { schema: {
|
|
@@ -665,7 +964,7 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
665
964
|
},
|
|
666
965
|
responses: {
|
|
667
966
|
200: {
|
|
668
|
-
description: "How many rows were deleted",
|
|
967
|
+
description: softDeleteNote ? "How many rows were moved to the trash, or deleted for good with `?hard=true`" : "How many rows were deleted",
|
|
669
968
|
content: { "application/json": { schema: {
|
|
670
969
|
type: "object",
|
|
671
970
|
properties: { meta: {
|
|
@@ -708,7 +1007,8 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
708
1007
|
schema: { type: "string" },
|
|
709
1008
|
description: "Comma-separated columns to return, as a SELECT projection. The primary key always survives and `excludeFromApi` columns stay hidden.",
|
|
710
1009
|
example: "id,title"
|
|
711
|
-
}
|
|
1010
|
+
},
|
|
1011
|
+
...deletedParameters(collection)
|
|
712
1012
|
],
|
|
713
1013
|
responses: {
|
|
714
1014
|
200: {
|
|
@@ -735,6 +1035,7 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
735
1035
|
delete: {
|
|
736
1036
|
tags: [collection.name],
|
|
737
1037
|
summary: `Delete ${collection.singularName || collection.name}`,
|
|
1038
|
+
...softDeleteNote ? { description: softDeleteNote } : {},
|
|
738
1039
|
operationId: `delete${schemaName}`,
|
|
739
1040
|
parameters: [
|
|
740
1041
|
{
|
|
@@ -751,10 +1052,11 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
751
1052
|
required: false,
|
|
752
1053
|
schema: { type: "string" },
|
|
753
1054
|
description: "Names this delete so a retry replays its answer. A delete replayed after the first attempt committed would otherwise answer 404 — which an offline queue reads as a permanent failure for a delete that in fact succeeded."
|
|
754
|
-
}
|
|
1055
|
+
},
|
|
1056
|
+
...hardDeleteParameters(collection)
|
|
755
1057
|
],
|
|
756
1058
|
responses: {
|
|
757
|
-
204: { description: "Deleted successfully" },
|
|
1059
|
+
204: { description: softDeleteNote ? "Moved to the trash: the row still exists and can be restored. With `?hard=true`, deleted for good" : "Deleted successfully" },
|
|
758
1060
|
404: {
|
|
759
1061
|
description: "Entity not found",
|
|
760
1062
|
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
|
|
@@ -781,34 +1083,44 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
781
1083
|
const relationName = relation.relationName;
|
|
782
1084
|
const targetSchema = schemaNameFor(relation.target());
|
|
783
1085
|
const subPath = `/data/${slug}/{parentId}/${relationName}`;
|
|
1086
|
+
const nestedParameters = [{
|
|
1087
|
+
name: "parentId",
|
|
1088
|
+
in: "path",
|
|
1089
|
+
required: true,
|
|
1090
|
+
schema: { type: "string" },
|
|
1091
|
+
description: `${collection.singularName || collection.name} ID`
|
|
1092
|
+
}, ...listQueryParameters()];
|
|
1093
|
+
const nestedResponses = {
|
|
1094
|
+
200: {
|
|
1095
|
+
description: `List of related ${relationName}`,
|
|
1096
|
+
content: { "application/json": { schema: {
|
|
1097
|
+
type: "object",
|
|
1098
|
+
properties: {
|
|
1099
|
+
data: {
|
|
1100
|
+
type: "array",
|
|
1101
|
+
items: schemas[targetSchema] ? { $ref: `#/components/schemas/${targetSchema}` } : { type: "object" }
|
|
1102
|
+
},
|
|
1103
|
+
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
1104
|
+
}
|
|
1105
|
+
} } }
|
|
1106
|
+
},
|
|
1107
|
+
...errorResponses(requireAuth)
|
|
1108
|
+
};
|
|
784
1109
|
paths[subPath] = { get: {
|
|
785
1110
|
tags: [collection.name],
|
|
786
1111
|
summary: `List ${relationName} for ${withIndefiniteArticle(collection.singularName || collection.name)}`,
|
|
787
1112
|
operationId: `list${schemaName}${toPascalCase(relationName)}`,
|
|
788
|
-
parameters:
|
|
789
|
-
|
|
790
|
-
in: "path",
|
|
791
|
-
required: true,
|
|
792
|
-
schema: { type: "string" },
|
|
793
|
-
description: `${collection.singularName || collection.name} ID`
|
|
794
|
-
}, ...listQueryParameters()],
|
|
795
|
-
responses: {
|
|
796
|
-
200: {
|
|
797
|
-
description: `List of related ${relationName}`,
|
|
798
|
-
content: { "application/json": { schema: {
|
|
799
|
-
type: "object",
|
|
800
|
-
properties: {
|
|
801
|
-
data: {
|
|
802
|
-
type: "array",
|
|
803
|
-
items: schemas[targetSchema] ? { $ref: `#/components/schemas/${targetSchema}` } : { type: "object" }
|
|
804
|
-
},
|
|
805
|
-
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
806
|
-
}
|
|
807
|
-
} } }
|
|
808
|
-
},
|
|
809
|
-
...errorResponses(requireAuth)
|
|
810
|
-
}
|
|
1113
|
+
parameters: nestedParameters,
|
|
1114
|
+
responses: nestedResponses
|
|
811
1115
|
} };
|
|
1116
|
+
paths[`${subPath}/query`] = { post: queryOperation({
|
|
1117
|
+
tags: [collection.name],
|
|
1118
|
+
summary: `List ${relationName} for ${withIndefiniteArticle(collection.singularName || collection.name)}, parameters in the body`,
|
|
1119
|
+
operationId: `query${schemaName}${toPascalCase(relationName)}`,
|
|
1120
|
+
parameters: nestedParameters,
|
|
1121
|
+
responses: nestedResponses,
|
|
1122
|
+
reserved: reservedParameterNames
|
|
1123
|
+
}) };
|
|
812
1124
|
}
|
|
813
1125
|
}
|
|
814
1126
|
return spec;
|
|
@@ -1104,6 +1416,10 @@ function updateOperation(collection, schemaName, requireAuth, shared) {
|
|
|
1104
1416
|
responses: {
|
|
1105
1417
|
200: {
|
|
1106
1418
|
description: "Updated entity",
|
|
1419
|
+
headers: { ETag: {
|
|
1420
|
+
schema: { type: "string" },
|
|
1421
|
+
description: "The version this write produced — the tag a `GET` of the row now answers. Send it as `If-Match` on the next write to chain conditional edits without reading the row back. Absent on an answer replayed from an `Idempotency-Key`."
|
|
1422
|
+
} },
|
|
1107
1423
|
content: { "application/json": { schema: { $ref: `#/components/schemas/${schemaName}` } } }
|
|
1108
1424
|
},
|
|
1109
1425
|
404: {
|
|
@@ -1157,7 +1473,7 @@ function buildCollectionInputSchema(collection) {
|
|
|
1157
1473
|
if ("isId" in property && property.isId && property.isId !== "manual" && property.isId !== true) continue;
|
|
1158
1474
|
properties[key] = convertPropertyToSchema(property);
|
|
1159
1475
|
emitted.add(key);
|
|
1160
|
-
if (property.validation?.required) required.push(key);
|
|
1476
|
+
if (property.validation?.required && !property.databaseDefault) required.push(key);
|
|
1161
1477
|
}
|
|
1162
1478
|
if (!emitted.has(idKey)) {
|
|
1163
1479
|
properties[idKey] = {
|
|
@@ -1390,6 +1706,65 @@ function buildFilterParameters(collection, reservedNames = /* @__PURE__ */ new S
|
|
|
1390
1706
|
return params;
|
|
1391
1707
|
}
|
|
1392
1708
|
/**
|
|
1709
|
+
* A `POST …/query` operation: the listing whose `parameters` are given, with
|
|
1710
|
+
* its query parameters moved into a JSON body.
|
|
1711
|
+
*
|
|
1712
|
+
* Built from the `GET`'s own parameter list, so the body documents exactly
|
|
1713
|
+
* what the route accepts — it turns the body back into those parameters and
|
|
1714
|
+
* runs the `GET`'s parser (`query-body.ts`). A filter parameter takes a list
|
|
1715
|
+
* where the URL would repeat it, and the four parameters whose URL value is
|
|
1716
|
+
* JSON take the JSON value itself.
|
|
1717
|
+
*/
|
|
1718
|
+
function queryOperation(operation) {
|
|
1719
|
+
const jsonValued = {
|
|
1720
|
+
where: { oneOf: [{
|
|
1721
|
+
type: "object",
|
|
1722
|
+
additionalProperties: true
|
|
1723
|
+
}, { type: "string" }] },
|
|
1724
|
+
include: { oneOf: [{
|
|
1725
|
+
type: "object",
|
|
1726
|
+
additionalProperties: true
|
|
1727
|
+
}, { type: "string" }] },
|
|
1728
|
+
orderBy: { oneOf: [{
|
|
1729
|
+
type: "array",
|
|
1730
|
+
items: {}
|
|
1731
|
+
}, { type: "string" }] },
|
|
1732
|
+
vector: { oneOf: [{
|
|
1733
|
+
type: "array",
|
|
1734
|
+
items: { type: "number" }
|
|
1735
|
+
}, { type: "string" }] }
|
|
1736
|
+
};
|
|
1737
|
+
const properties = {};
|
|
1738
|
+
for (const parameter of operation.parameters) {
|
|
1739
|
+
if (parameter.in !== "query") continue;
|
|
1740
|
+
const name = String(parameter.name);
|
|
1741
|
+
const schema = parameter.schema;
|
|
1742
|
+
const isFilter = !operation.reserved.has(name);
|
|
1743
|
+
properties[name] = {
|
|
1744
|
+
...jsonValued[name] ?? (isFilter ? { oneOf: [schema, {
|
|
1745
|
+
type: "array",
|
|
1746
|
+
items: schema
|
|
1747
|
+
}] } : schema),
|
|
1748
|
+
...parameter.description ? { description: parameter.description } : {}
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
return {
|
|
1752
|
+
tags: operation.tags,
|
|
1753
|
+
summary: operation.summary,
|
|
1754
|
+
description: operation.description ?? "The listing `GET` answers, with its query parameters in a JSON body instead of the URL: the same filters, page, count, cursor and includes, the same row-level security, and the same errors. It reads and never writes, so a credential needs `data:read` for it. Use it when the URL would not fit: a vector search's embedding is a JSON array in `vector`, and 768 dimensions of it is 17 KB of URL, past what Node and most proxies accept. A parameter also on the URL is a 400 INVALID_QUERY_BODY.",
|
|
1755
|
+
operationId: operation.operationId,
|
|
1756
|
+
parameters: operation.parameters.filter((parameter) => parameter.in !== "query"),
|
|
1757
|
+
requestBody: {
|
|
1758
|
+
required: true,
|
|
1759
|
+
content: { "application/json": { schema: {
|
|
1760
|
+
type: "object",
|
|
1761
|
+
properties
|
|
1762
|
+
} } }
|
|
1763
|
+
},
|
|
1764
|
+
responses: operation.responses
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1393
1768
|
* Standard error responses included on every endpoint.
|
|
1394
1769
|
*/
|
|
1395
1770
|
function errorResponses(requireAuth) {
|
|
@@ -1446,6 +1821,6 @@ function toPascalCase(str) {
|
|
|
1446
1821
|
return str.replace(/[^a-zA-Z0-9]+/g, " ").split(" ").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
1447
1822
|
}
|
|
1448
1823
|
//#endregion
|
|
1449
|
-
export {
|
|
1824
|
+
export { softDeleteFieldOf as a, JUNCTION_PIVOT_KEY as c, restoresSoftDeletedRow as i, hasForeignKeyOnTarget as l, buildCollectionSchema as n, buildTenantSecurityRule as o, openapi_generator_exports as r, INCLUDE_COUNT_KEY as s, buildCollectionInputSchema as t, isManyToMany as u };
|
|
1450
1825
|
|
|
1451
|
-
//# sourceMappingURL=openapi-generator-
|
|
1826
|
+
//# sourceMappingURL=openapi-generator-BqoUh4Ij.js.map
|