@rebasepro/server 0.22.0 → 0.24.0
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-CLIJXwGS.js → GCSStorageController-BSiP1c-f.js} +57 -29
- package/dist/GCSStorageController-BSiP1c-f.js.map +1 -0
- package/dist/{S3StorageController-Dcuf8lMA.js → S3StorageController-CAwFRgjV.js} +19 -7
- package/dist/S3StorageController-CAwFRgjV.js.map +1 -0
- package/dist/api/ast-schema-editor.d.ts +127 -1
- package/dist/api/errors.d.ts +9 -0
- package/dist/api/live-schema-routes.d.ts +52 -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 +158 -42
- package/dist/api/rest/auth-collection-writes.d.ts +85 -0
- package/dist/api/rest/field-access-query.d.ts +6 -2
- package/dist/api/rest/idempotency.d.ts +7 -1
- package/dist/api/rest/nested-write-access.d.ts +46 -0
- package/dist/api/rest/write-validation.d.ts +34 -2
- package/dist/api/types.d.ts +17 -1
- package/dist/{ast-schema-editor-CslO8Oje.js → ast-schema-editor-CWqS_sLJ.js} +411 -13
- package/dist/ast-schema-editor-CWqS_sLJ.js.map +1 -0
- package/dist/auth/access.d.ts +105 -0
- package/dist/auth/adapter-middleware.d.ts +2 -1
- package/dist/auth/address-ownership.d.ts +68 -0
- 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 +35 -2
- package/dist/auth/admin-users-route.d.ts +1 -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 +50 -7
- package/dist/auth/builtin-auth-adapter.d.ts +8 -0
- package/dist/auth/captcha.d.ts +5 -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 +67 -0
- package/dist/auth/impersonation.d.ts +110 -0
- package/dist/auth/index.d.ts +4 -2
- package/dist/auth/interfaces.d.ts +146 -65
- package/dist/auth/jwt.d.ts +66 -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/oauth-signin-policy.d.ts +25 -8
- 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 +72 -1
- package/dist/auth/routes.d.ts +45 -0
- package/dist/auth/scope-routes.d.ts +22 -0
- package/dist/auth/session-routes.d.ts +18 -6
- package/dist/auth/token-revocation.d.ts +53 -1
- package/dist/auth/verify-credential.d.ts +28 -0
- package/dist/{auth-CCDpk2rn.js → auth-DMLngxn_.js} +2712 -711
- package/dist/auth-DMLngxn_.js.map +1 -0
- package/dist/backend-DTAOsLQc.js +30 -0
- package/dist/backend-DTAOsLQc.js.map +1 -0
- package/dist/backup/backup-common.d.ts +29 -0
- package/dist/backup/backup-routes.d.ts +24 -4
- package/dist/backup/backup-schedule.d.ts +33 -0
- package/dist/backup/backup-storage.d.ts +14 -0
- package/dist/backup/index.d.ts +2 -0
- package/dist/backup-CN0s50D2.js +444 -0
- package/dist/backup-CN0s50D2.js.map +1 -0
- package/dist/boot/bundle.d.ts +19 -0
- package/dist/boot/driver.d.ts +10 -0
- package/dist/boot/env.d.ts +51 -6
- package/dist/boot/fetch-bundle.d.ts +18 -1
- package/dist/boot/rls-audit-option.d.ts +26 -0
- package/dist/boot/security-headers.d.ts +26 -0
- package/dist/boot/sources.d.ts +1 -0
- package/dist/boot/static-routing.d.ts +56 -0
- package/dist/collection_patch-BRu-BvDv.js +472 -0
- package/dist/collection_patch-BRu-BvDv.js.map +1 -0
- package/dist/{contract-routes-eLxV0le1.js → contract-routes-fz8i4pxs.js} +17 -4
- package/dist/contract-routes-fz8i4pxs.js.map +1 -0
- package/dist/cron/cron-routes.d.ts +7 -2
- package/dist/cron/cron-scheduler.d.ts +146 -21
- package/dist/cron/cron-store.d.ts +76 -8
- package/dist/cron/index.d.ts +1 -1
- package/dist/{cron-loader-CQjvjpEw.js → cron-loader-CwaANlOG.js} +4 -4
- package/dist/cron-loader-CwaANlOG.js.map +1 -0
- package/dist/cron-routes-Bc-SB0Se.js +96 -0
- package/dist/cron-routes-Bc-SB0Se.js.map +1 -0
- package/dist/{cron-scheduler-COPQxlEq.js → cron-scheduler-CYQgco86.js} +427 -83
- package/dist/cron-scheduler-CYQgco86.js.map +1 -0
- package/dist/{cron-store-BYGZFNWk.js → cron-store-D2Q9-Aco.js} +139 -23
- package/dist/cron-store-D2Q9-Aco.js.map +1 -0
- package/dist/{ddl-bootstrap-CfNvxMuK.js → ddl-bootstrap-BaqMSa4Y.js} +3 -26
- package/dist/ddl-bootstrap-BaqMSa4Y.js.map +1 -0
- package/dist/email/index.d.ts +2 -2
- package/dist/email/link-base.d.ts +5 -4
- package/dist/email/smtp-email-service.d.ts +13 -1
- package/dist/email/templates.d.ts +31 -0
- package/dist/email/types.d.ts +29 -2
- package/dist/env.d.ts +25 -7
- package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
- package/dist/errors-D6_y86c5.js.map +1 -0
- package/dist/{function-loader-xnbDAPfa.js → function-loader-D7o5Epjj.js} +2 -2
- package/dist/{function-loader-xnbDAPfa.js.map → function-loader-D7o5Epjj.js.map} +1 -1
- package/dist/{function-routes-Chet4-lB.js → function-routes-CaNG4waN.js} +24 -12
- package/dist/function-routes-CaNG4waN.js.map +1 -0
- package/dist/functions/context.d.ts +17 -6
- package/dist/functions/guards.d.ts +22 -5
- package/dist/functions/index.d.ts +2 -2
- package/dist/functions/index.js +90 -36
- package/dist/functions/index.js.map +1 -1
- package/dist/{history-recorder-BQmB0P_j.js → history-recorder-Nr8zLvoU.js} +9 -7
- package/dist/history-recorder-Nr8zLvoU.js.map +1 -0
- package/dist/{history-store-CetkrBBD.js → history-store-rcAm_xFR.js} +2 -2
- package/dist/{history-store-CetkrBBD.js.map → history-store-rcAm_xFR.js.map} +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.es.js +5733 -1551
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +5 -2
- package/dist/init/health.d.ts +17 -2
- package/dist/init/shutdown.d.ts +18 -3
- package/dist/init.d.ts +54 -0
- package/dist/jobs/index.d.ts +2 -2
- package/dist/jobs/job-queue.d.ts +23 -2
- package/dist/jobs/job-store.d.ts +37 -5
- package/dist/jobs/types.d.ts +8 -6
- package/dist/{jobs-Bjr8DZAi.js → jobs-DqYNfquG.js} +306 -167
- package/dist/jobs-DqYNfquG.js.map +1 -0
- package/dist/{jwt-C4OW-DNq.js → jwt-R6bSPMjk.js} +114 -38
- package/dist/{jwt-C4OW-DNq.js.map → jwt-R6bSPMjk.js.map} +1 -1
- package/dist/{keys-Qfc4XieN.js → keys-GAVZqbqx.js} +18 -17
- package/dist/{keys-Qfc4XieN.js.map → keys-GAVZqbqx.js.map} +1 -1
- 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-3EEzPjhl.js → logs-routes-DAdv37GI.js} +54 -11
- 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 +45 -2
- package/dist/mcp/mcp-tools.d.ts +22 -10
- package/dist/mcp/oauth-metadata.d.ts +21 -16
- package/dist/mcp/oauth-routes.d.ts +34 -1
- package/dist/mcp/oauth-store.d.ts +29 -13
- package/dist/metrics/history-recorder.d.ts +1 -1
- package/dist/{openapi-generator-D8uFz-LW.js → openapi-generator-DAq_XVDu.js} +135 -22
- package/dist/openapi-generator-DAq_XVDu.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-BleZmY18.js → query-parser-BgiKJKvc.js} +41 -82
- package/dist/query-parser-BgiKJKvc.js.map +1 -0
- package/dist/{request-timeout-C_4C2BeR.js → request-timeout-DgH7j8qO.js} +3 -3
- package/dist/{request-timeout-C_4C2BeR.js.map → request-timeout-DgH7j8qO.js.map} +1 -1
- package/dist/rls-audit/index.d.ts +4 -0
- package/dist/schema-edit/apply-schema-change.d.ts +63 -3
- package/dist/schema-edit/project-root.d.ts +3 -2
- package/dist/schema-edit/remote-source.d.ts +9 -4
- package/dist/{schema-editor-routes-DdLihzp0.js → schema-editor-routes-oIyuWl3L.js} +12 -7
- package/dist/schema-editor-routes-oIyuWl3L.js.map +1 -0
- package/dist/serve-spa.d.ts +58 -0
- package/dist/services/routed-realtime-service.d.ts +11 -0
- package/dist/soft-delete-params-BWPilMPF.js +59 -0
- package/dist/soft-delete-params-BWPilMPF.js.map +1 -0
- package/dist/{src-Br6ARbs6.js → src-CatHFUym.js} +439 -45
- package/dist/src-CatHFUym.js.map +1 -0
- package/dist/{src-1vL-I1Po.js → src-I3aG1PcY.js} +371 -81
- package/dist/src-I3aG1PcY.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +13 -1
- package/dist/storage/LocalStorageController.d.ts +2 -0
- package/dist/storage/S3StorageController.d.ts +2 -0
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/keys.d.ts +12 -0
- package/dist/storage/property-limits.d.ts +41 -6
- package/dist/storage/rendition-cache.d.ts +11 -1
- package/dist/storage/request-keys.d.ts +82 -0
- package/dist/storage/requested-object.d.ts +74 -0
- package/dist/storage/routes.d.ts +36 -18
- package/dist/storage/tus-handler.d.ts +30 -5
- package/dist/storage/types.d.ts +36 -1
- package/dist/types-BfKcm9do.js.map +1 -1
- package/dist/utils/logger.d.ts +12 -0
- package/package.json +9 -9
- package/dist/GCSStorageController-CLIJXwGS.js.map +0 -1
- package/dist/S3StorageController-Dcuf8lMA.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-CslO8Oje.js.map +0 -1
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
- package/dist/auth-CCDpk2rn.js.map +0 -1
- package/dist/backup-DzI9jLwc.js +0 -192
- package/dist/backup-DzI9jLwc.js.map +0 -1
- package/dist/contract-routes-eLxV0le1.js.map +0 -1
- package/dist/cron-loader-CQjvjpEw.js.map +0 -1
- package/dist/cron-routes-B7CRGfiq.js +0 -72
- package/dist/cron-routes-B7CRGfiq.js.map +0 -1
- package/dist/cron-scheduler-COPQxlEq.js.map +0 -1
- package/dist/cron-store-BYGZFNWk.js.map +0 -1
- package/dist/ddl-bootstrap-CfNvxMuK.js.map +0 -1
- package/dist/errors-DWsX4yTd.js.map +0 -1
- package/dist/function-routes-Chet4-lB.js.map +0 -1
- package/dist/history-recorder-BQmB0P_j.js.map +0 -1
- package/dist/jobs-Bjr8DZAi.js.map +0 -1
- package/dist/logger-DO2PZc4i.js.map +0 -1
- package/dist/logs-routes-3EEzPjhl.js.map +0 -1
- package/dist/openapi-generator-D8uFz-LW.js.map +0 -1
- package/dist/query-parser-BleZmY18.js.map +0 -1
- package/dist/schema-editor-routes-DdLihzp0.js.map +0 -1
- package/dist/src-1vL-I1Po.js.map +0 -1
- package/dist/src-Br6ARbs6.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Database-Agnostic Backend Core for Rebase",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rebase",
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"jsonwebtoken": "^9.0.3",
|
|
55
55
|
"ws": "^8.21.1",
|
|
56
56
|
"zod": "^4.4.3",
|
|
57
|
-
"@rebasepro/
|
|
58
|
-
"@rebasepro/
|
|
59
|
-
"@rebasepro/utils": "0.
|
|
60
|
-
"@rebasepro/
|
|
57
|
+
"@rebasepro/client": "0.24.0",
|
|
58
|
+
"@rebasepro/common": "0.24.0",
|
|
59
|
+
"@rebasepro/utils": "0.24.0",
|
|
60
|
+
"@rebasepro/types": "0.24.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@jest/globals": "^30.4.1",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"@types/ws": "^8.18.1",
|
|
69
69
|
"cross-env": "^10.1.0",
|
|
70
70
|
"fast-check": "^4.9.0",
|
|
71
|
-
"hono": "^4.13.
|
|
71
|
+
"hono": "^4.13.5",
|
|
72
72
|
"jest": "^30.4.2",
|
|
73
73
|
"ts-jest": "29.4.12",
|
|
74
74
|
"typescript": "^6.0.3",
|
|
@@ -80,9 +80,9 @@
|
|
|
80
80
|
"@aws-sdk/s3-request-presigner": "^3.1068.0",
|
|
81
81
|
"@google-cloud/storage": "^7.0.0",
|
|
82
82
|
"google-auth-library": "^10.7.0",
|
|
83
|
-
"hono": "^4.
|
|
84
|
-
"nodemailer": "^9.
|
|
85
|
-
"sharp": "^0.35.
|
|
83
|
+
"hono": "^4.13.5",
|
|
84
|
+
"nodemailer": "^9.1.1",
|
|
85
|
+
"sharp": "^0.35.4",
|
|
86
86
|
"ts-morph": "28.0.0"
|
|
87
87
|
},
|
|
88
88
|
"peerDependenciesMeta": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GCSStorageController-CLIJXwGS.js","names":[],"sources":["../src/storage/GCSStorageController.ts"],"sourcesContent":["/**\n * Google Cloud Storage controller (works with GCS and Firebase Storage)\n *\n * The `@google-cloud/storage` package is loaded lazily so it remains an\n * optional peer dependency — users who don't need GCS never pay for the\n * import cost or need it installed.\n */\n\nimport {\n StorageController,\n GCSStorageConfig,\n DEFAULT_MAX_FILE_SIZE\n} from \"./types\";\nimport { folderKey, listingPrefix } from \"./keys\";\nimport {\n UploadFileProps,\n UploadFileResult,\n DownloadConfig,\n DownloadMetadata,\n StorageListResult,\n StorageReference\n} from \"@rebasepro/types\";\n\n// ---------------------------------------------------------------------------\n// Lazy import of @google-cloud/storage\n// ---------------------------------------------------------------------------\n\nlet gcsModule: typeof import(\"@google-cloud/storage\") | null = null;\n\nasync function loadGCS(): Promise<typeof import(\"@google-cloud/storage\")> {\n if (gcsModule) return gcsModule;\n try {\n gcsModule = await import(\"@google-cloud/storage\");\n return gcsModule;\n } catch {\n throw new Error(\n \"Google Cloud Storage is required for the GCS storage controller. \" +\n \"Install it with: pnpm add @google-cloud/storage\"\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// GCSStorageController\n// ---------------------------------------------------------------------------\n\n/**\n * Google Cloud Storage implementation of `StorageController`.\n *\n * Works with standard GCS buckets **and** Firebase Storage buckets\n * (e.g. `\"my-project.appspot.com\"`).\n */\nexport class GCSStorageController implements StorageController {\n private config: GCSStorageConfig;\n private client: InstanceType<typeof import(\"@google-cloud/storage\").Storage> | null = null;\n\n constructor(config: GCSStorageConfig) {\n this.config = config;\n }\n\n // ------------------------------------------------------------------\n // Lazy client initialisation\n // ------------------------------------------------------------------\n\n private async getClient(): Promise<InstanceType<typeof import(\"@google-cloud/storage\").Storage>> {\n if (this.client) return this.client;\n const { Storage } = await loadGCS();\n this.client = new Storage({\n projectId: this.config.projectId,\n keyFilename: this.config.keyFilename,\n credentials: this.config.credentials\n });\n return this.client;\n }\n\n // ------------------------------------------------------------------\n // StorageController interface\n // ------------------------------------------------------------------\n\n getType(): \"gcs\" {\n return \"gcs\";\n }\n\n /**\n * The configured bucket, and the logical `default` that maps to it.\n * @see S3StorageController.knownBuckets\n */\n knownBuckets(): string[] {\n return [\"default\", this.config.bucket];\n }\n\n async putObject({\n file,\n key,\n metadata,\n bucket\n }: UploadFileProps): Promise<UploadFileResult> {\n this.validateFile(file);\n\n const usedBucket = this.getBucket(bucket);\n const client = await this.getClient();\n const gcsFile = client.bucket(usedBucket).file(key);\n\n // Convert File to Buffer\n const arrayBuffer = await file.arrayBuffer();\n const buffer = Buffer.from(arrayBuffer);\n\n await gcsFile.save(buffer, {\n contentType: file.type,\n metadata: metadata ? { metadata: this.flattenMetadata(metadata) } : undefined\n });\n\n return {\n key,\n bucket: usedBucket,\n storageUrl: `gs://${usedBucket}/${key}`\n };\n }\n\n async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {\n // Handle gs:// and s3:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(gs|s3):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const client = await this.getClient();\n const gcsFile = client.bucket(resolvedBucket).file(resolvedPath);\n\n // Get metadata first to check existence and populate response\n const [fileMetadata] = await gcsFile.getMetadata();\n\n // Generate a signed URL\n const expiresIn = this.config.signedUrlExpiration ?? 3600;\n const [url] = await gcsFile.getSignedUrl({\n action: \"read\",\n expires: Date.now() + expiresIn * 1000\n });\n\n const metadata: DownloadMetadata = {\n bucket: resolvedBucket,\n fullPath: resolvedPath,\n name: resolvedPath.split(\"/\").pop() || resolvedPath,\n size: Number(fileMetadata.size) || 0,\n contentType: fileMetadata.contentType || \"application/octet-stream\",\n customMetadata: (fileMetadata.metadata as Record<string, string> | undefined) || {}\n };\n\n return {\n url,\n metadata\n };\n } catch (error: unknown) {\n if (isNotFoundError(error)) {\n return {\n url: null,\n fileNotFound: true\n };\n }\n throw error;\n }\n }\n\n async getObject(key: string, bucket?: string): Promise<File | null> {\n // Handle gs:// and s3:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(gs|s3):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const client = await this.getClient();\n const gcsFile = client.bucket(resolvedBucket).file(resolvedPath);\n\n const [contents] = await gcsFile.download();\n\n // Get content type from metadata\n const [fileMetadata] = await gcsFile.getMetadata();\n const contentType = fileMetadata.contentType || \"application/octet-stream\";\n const fileName = resolvedPath.split(\"/\").pop() || resolvedPath;\n\n const blob = new Blob([new Uint8Array(contents)], { type: contentType });\n return new File([blob], fileName, { type: contentType });\n } catch (error: unknown) {\n if (isNotFoundError(error)) {\n return null;\n }\n throw error;\n }\n }\n\n async deleteObject(key: string, bucket?: string): Promise<void> {\n // Handle gs:// and s3:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(gs|s3):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n const client = await this.getClient();\n const gcsFile = client.bucket(resolvedBucket).file(resolvedPath);\n\n await gcsFile.delete({ ignoreNotFound: true });\n }\n\n async listObjects(prefix: string, options?: {\n bucket?: string;\n maxResults?: number;\n pageToken?: string;\n }): Promise<StorageListResult> {\n const resolvedBucket = this.getBucket(options?.bucket);\n const client = await this.getClient();\n const gcsBucket = client.bucket(resolvedBucket);\n\n // Fetch files (objects) matching the prefix\n const [files, , filesApiResponse] = await gcsBucket.getFiles({\n prefix: listingPrefix(prefix),\n delimiter: \"/\",\n maxResults: options?.maxResults ?? 1000,\n pageToken: options?.pageToken,\n autoPaginate: false\n });\n\n const items: StorageReference[] = files.map(file => ({\n bucket: resolvedBucket,\n fullPath: file.name,\n name: file.name.split(\"/\").pop() || \"\",\n parent: null as never,\n root: null as never,\n toString: () => `gs://${resolvedBucket}/${file.name}`\n }));\n\n // Common prefixes (folder-like entries) come from the API response\n const apiPrefixes: string[] =\n (filesApiResponse as Record<string, unknown> | undefined)?.prefixes as string[] ?? [];\n\n const prefixes: StorageReference[] = apiPrefixes.map(p => {\n // Without the trailing slash GCS puts on a common prefix, so a\n // `fullPath` from a listing is a key any controller accepts.\n const folder = folderKey(p);\n return {\n bucket: resolvedBucket,\n fullPath: folder,\n name: folder.split(\"/\").pop() || \"\",\n parent: null as never,\n root: null as never,\n toString: () => `gs://${resolvedBucket}/${folder}`\n };\n });\n\n // The next page token is returned on the query object (second element)\n const nextPageToken =\n (filesApiResponse as Record<string, unknown> | undefined)?.nextPageToken as string | undefined;\n\n return {\n items,\n prefixes,\n nextPageToken\n };\n }\n\n // ------------------------------------------------------------------\n // Private helpers\n // ------------------------------------------------------------------\n\n /**\n * Validate file before upload\n */\n private validateFile(file: File): void {\n const maxSize = this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;\n if (file.size > maxSize) {\n throw new Error(`File size ${file.size} exceeds maximum allowed size ${maxSize}`);\n }\n\n if (this.config.allowedMimeTypes && this.config.allowedMimeTypes.length > 0) {\n if (!this.config.allowedMimeTypes.includes(file.type)) {\n throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedMimeTypes.join(\", \")}`);\n }\n }\n }\n\n /**\n * Get the bucket name — either from parameter or config.\n */\n private getBucket(bucket?: string): string {\n // \"default\" is a logical bucket name used by local storage;\n // for GCS it should resolve to the configured bucket.\n if (!bucket || bucket === \"default\") return this.config.bucket;\n return bucket;\n }\n\n /**\n * Flatten nested metadata to string values (GCS custom metadata requirement)\n */\n private flattenMetadata(metadata: Record<string, unknown>): Record<string, string> {\n const flattened: Record<string, string> = {};\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === \"string\") {\n flattened[key] = value;\n } else if (value !== undefined && value !== null) {\n flattened[key] = JSON.stringify(value);\n }\n }\n return flattened;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Error helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether an error represents a 404 / Not Found from the GCS client.\n */\nfunction isNotFoundError(error: unknown): boolean {\n if (typeof error !== \"object\" || error === null) return false;\n const err = error as Record<string, unknown>;\n if (err[\"code\"] === 404) return true;\n if (typeof err[\"message\"] === \"string\" && /not found/i.test(err[\"message\"])) return true;\n return false;\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"S3StorageController-Dcuf8lMA.js","names":[],"sources":["../src/storage/S3StorageController.ts"],"sourcesContent":["/**\n * S3-compatible storage controller (works with AWS S3 and MinIO)\n */\n\nimport type { S3Client as S3ClientType, } from \"@aws-sdk/client-s3\";\nimport { DEFAULT_MAX_FILE_SIZE, S3StorageConfig, StorageController } from \"./types\";\nimport { folderKey, listingPrefix } from \"./keys\";\nimport {\n DownloadConfig,\n DownloadMetadata,\n StorageListResult,\n StorageReference,\n UploadFileProps,\n UploadFileResult\n} from \"@rebasepro/types\";\n\nlet _s3Module: typeof import(\"@aws-sdk/client-s3\") | undefined;\nlet _presignerModule: typeof import(\"@aws-sdk/s3-request-presigner\") | undefined;\n\nasync function loadS3() {\n if (!_s3Module) {\n try {\n _s3Module = await import(\"@aws-sdk/client-s3\");\n } catch {\n throw new Error(\n \"@aws-sdk/client-s3 is required for S3 storage. \" +\n \"Install it: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner\"\n );\n }\n }\n return _s3Module;\n}\n\nasync function loadPresigner() {\n if (!_presignerModule) {\n try {\n _presignerModule = await import(\"@aws-sdk/s3-request-presigner\");\n } catch {\n throw new Error(\n \"@aws-sdk/s3-request-presigner is required for S3 storage. \" +\n \"Install it: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner\"\n );\n }\n }\n return _presignerModule;\n}\n\n/**\n * S3-compatible storage implementation\n * Works with AWS S3 and MinIO (with forcePathStyle option)\n */\nexport class S3StorageController implements StorageController {\n private config: S3StorageConfig;\n private _client: S3ClientType | undefined;\n\n constructor(config: S3StorageConfig) {\n this.config = config;\n }\n\n /**\n * Lazily create and cache the S3 client on first use\n */\n private async getClient(): Promise<S3ClientType> {\n if (!this._client) {\n const s3 = await loadS3();\n this._client = new s3.S3Client({\n region: this.config.region || \"us-east-1\",\n endpoint: this.config.endpoint,\n forcePathStyle: this.config.forcePathStyle ?? !!this.config.endpoint, // Auto-enable for custom endpoints (MinIO)\n credentials: {\n accessKeyId: this.config.accessKeyId,\n secretAccessKey: this.config.secretAccessKey\n }\n });\n }\n return this._client;\n }\n\n getType(): \"s3\" {\n return \"s3\";\n }\n\n /**\n * The configured bucket, and the logical `default` that maps to it.\n *\n * Any other name went straight to S3 as a bucket name, so the request\n * parameter was a way to address *any* bucket the deployment's credentials\n * can reach — and a mistyped one came back as \"file not found\".\n */\n knownBuckets(): string[] {\n return [\"default\", this.config.bucket];\n }\n\n /**\n * Validate file before upload\n */\n private validateFile(file: File): void {\n const maxSize = this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;\n if (file.size > maxSize) {\n throw new Error(`File size ${file.size} exceeds maximum allowed size ${maxSize}`);\n }\n\n if (this.config.allowedMimeTypes && this.config.allowedMimeTypes.length > 0) {\n if (!this.config.allowedMimeTypes.includes(file.type)) {\n throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedMimeTypes.join(\", \")}`);\n }\n }\n }\n\n /**\n * Get the bucket name - either from parameter or config\n */\n private getBucket(bucket?: string): string {\n // \"default\" is a logical bucket name used by local storage;\n // for S3 it should resolve to the configured bucket.\n if (!bucket || bucket === \"default\") return this.config.bucket;\n return bucket;\n }\n\n async putObject({\n file,\n key,\n metadata,\n bucket\n }: UploadFileProps): Promise<UploadFileResult> {\n this.validateFile(file);\n\n const usedBucket = this.getBucket(bucket);\n\n // Convert File to Buffer\n const arrayBuffer = await file.arrayBuffer();\n const buffer = Buffer.from(arrayBuffer);\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.PutObjectCommand({\n Bucket: usedBucket,\n Key: key,\n Body: buffer,\n ContentType: file.type,\n Metadata: metadata ? this.flattenMetadata(metadata) : undefined\n });\n\n await client.send(command);\n\n return {\n key,\n bucket: usedBucket,\n storageUrl: `s3://${usedBucket}/${key}`\n };\n }\n\n /**\n * Flatten nested metadata to string values (S3 requirement)\n */\n private flattenMetadata(metadata: Record<string, unknown>): Record<string, string> {\n const flattened: Record<string, string> = {};\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === \"string\") {\n flattened[key] = value;\n } else if (value !== undefined && value !== null) {\n flattened[key] = JSON.stringify(value);\n }\n }\n return flattened;\n }\n\n async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {\n // Handle s3:// and gs:// URLs for backward compatibility\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const s3 = await loadS3();\n const presigner = await loadPresigner();\n const client = await this.getClient();\n\n // First check if the object exists and get metadata\n const headCommand = new s3.HeadObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n const headResult = await client.send(headCommand);\n\n // Generate a signed URL\n const getCommand = new s3.GetObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n const expiresIn = this.config.signedUrlExpiration ?? 3600;\n const url = await presigner.getSignedUrl(client, getCommand, { expiresIn });\n\n const metadata: DownloadMetadata = {\n bucket: resolvedBucket,\n fullPath: resolvedPath,\n name: resolvedPath.split(\"/\").pop() || resolvedPath,\n size: headResult.ContentLength || 0,\n contentType: headResult.ContentType || \"application/octet-stream\",\n customMetadata: headResult.Metadata || {}\n };\n\n return {\n url,\n metadata\n };\n } catch (error: unknown) {\n const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };\n if (s3Error.name === \"NotFound\" || s3Error.$metadata?.httpStatusCode === 404) {\n return {\n url: null,\n fileNotFound: true\n };\n }\n throw error;\n }\n }\n\n async getObject(key: string, bucket?: string): Promise<File | null> {\n // Handle s3:// and gs:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.GetObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n const response = await client.send(command);\n\n if (!response.Body) {\n return null;\n }\n\n // Convert stream to buffer\n const chunks: Uint8Array[] = [];\n for await (const chunk of response.Body as AsyncIterable<Uint8Array>) {\n chunks.push(chunk);\n }\n const buffer = Buffer.concat(chunks);\n\n const contentType = response.ContentType || \"application/octet-stream\";\n const fileName = resolvedPath.split(\"/\").pop() || resolvedPath;\n\n const blob = new Blob([buffer], { type: contentType });\n return new File([blob], fileName, { type: contentType });\n } catch (error: unknown) {\n const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };\n if (s3Error.name === \"NoSuchKey\" || s3Error.$metadata?.httpStatusCode === 404) {\n return null;\n }\n throw error;\n }\n }\n\n async deleteObject(key: string, bucket?: string): Promise<void> {\n // Handle s3:// and gs:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.DeleteObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n // Deleting something that is already gone is the outcome the caller\n // wanted, so it is not an error. AWS itself answers DeleteObject with\n // 204 for a key that never existed, but S3-compatible backends (MinIO,\n // Ceph, R2 in some configurations) do raise NoSuchKey — and the local\n // controller has always returned quietly in this case. Without this,\n // which of those a deployment happens to run decides whether\n // `DELETE /api/storage/:path` on a stale reference is a 200 or a 500.\n try {\n await client.send(command);\n } catch (error: unknown) {\n const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };\n if (s3Error.name === \"NoSuchKey\" || s3Error.$metadata?.httpStatusCode === 404) {\n return;\n }\n throw error;\n }\n }\n\n async listObjects(prefix: string, options?: {\n bucket?: string;\n maxResults?: number;\n pageToken?: string;\n }): Promise<StorageListResult> {\n const resolvedBucket = this.getBucket(options?.bucket);\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.ListObjectsV2Command({\n Bucket: resolvedBucket,\n Prefix: listingPrefix(prefix),\n MaxKeys: options?.maxResults ?? 1000,\n ContinuationToken: options?.pageToken,\n Delimiter: \"/\" // This gives us folder-like behavior\n });\n\n const response = await client.send(command);\n\n const items: StorageReference[] = (response.Contents || []).map(obj => ({\n bucket: resolvedBucket,\n fullPath: obj.Key || \"\",\n name: (obj.Key || \"\").split(\"/\").pop() || \"\",\n parent: null as never,\n root: null as never,\n toString: () => `s3://${resolvedBucket}/${obj.Key}`\n }));\n\n const prefixes: StorageReference[] = (response.CommonPrefixes || []).map(common => {\n // Without the trailing slash S3 puts on a common prefix, so a\n // `fullPath` from a listing is a key any controller accepts.\n const folder = folderKey(common.Prefix || \"\");\n return {\n bucket: resolvedBucket,\n fullPath: folder,\n name: folder.split(\"/\").pop() || \"\",\n parent: null as never,\n root: null as never,\n toString: () => `s3://${resolvedBucket}/${folder}`\n };\n });\n\n return {\n items,\n prefixes,\n nextPageToken: response.NextContinuationToken\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { createRequire as __rebaseCreateRequire } from "module";
|
|
2
|
-
import __rebaseProcess from "process";
|
|
3
|
-
globalThis.process ??= __rebaseProcess;
|
|
4
|
-
__rebaseCreateRequire(import.meta.url);
|
|
5
|
-
//#region src/auth/admin-roles.ts
|
|
6
|
-
/**
|
|
7
|
-
* Which role names carry administrative privilege.
|
|
8
|
-
*
|
|
9
|
-
* One definition, because there used to be several and they disagreed.
|
|
10
|
-
* `requireAdmin` accepted `admin` **or** `schema-admin`; the guard that refuses
|
|
11
|
-
* a dangerous `defaultRole` compared against `admin` alone. So
|
|
12
|
-
* `AUTH_DEFAULT_ROLE=schema-admin` passed the guard and made every public
|
|
13
|
-
* registrant an administrator — and, since a `schema-admin` may edit users, one
|
|
14
|
-
* of them could then grant themselves real `admin` and keep it.
|
|
15
|
-
*
|
|
16
|
-
* The guard and the check have to read the same list or the gap comes back, so
|
|
17
|
-
* neither of them owns it.
|
|
18
|
-
*
|
|
19
|
-
* `schema-admin` is here deliberately: it can reach the schema editor and the
|
|
20
|
-
* SQL surfaces, which is administrative by any definition that matters. If a
|
|
21
|
-
* genuinely lesser role is added later, it does not belong in this list — it
|
|
22
|
-
* belongs in a capability check of its own.
|
|
23
|
-
*/
|
|
24
|
-
var ADMINISTRATIVE_ROLES = ["admin", "schema-admin"];
|
|
25
|
-
/** Does this single role name carry administrative privilege? */
|
|
26
|
-
function isAdministrativeRole(role) {
|
|
27
|
-
return ADMINISTRATIVE_ROLES.includes(role);
|
|
28
|
-
}
|
|
29
|
-
/** Does any role in this list carry administrative privilege? */
|
|
30
|
-
function hasAdministrativeRole(roles) {
|
|
31
|
-
return !!roles?.some(isAdministrativeRole);
|
|
32
|
-
}
|
|
33
|
-
//#endregion
|
|
34
|
-
export { hasAdministrativeRole as n, isAdministrativeRole as r, ADMINISTRATIVE_ROLES as t };
|
|
35
|
-
|
|
36
|
-
//# sourceMappingURL=admin-roles-vYdp_Pil.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"admin-roles-vYdp_Pil.js","names":[],"sources":["../src/auth/admin-roles.ts"],"sourcesContent":["/**\n * Which role names carry administrative privilege.\n *\n * One definition, because there used to be several and they disagreed.\n * `requireAdmin` accepted `admin` **or** `schema-admin`; the guard that refuses\n * a dangerous `defaultRole` compared against `admin` alone. So\n * `AUTH_DEFAULT_ROLE=schema-admin` passed the guard and made every public\n * registrant an administrator — and, since a `schema-admin` may edit users, one\n * of them could then grant themselves real `admin` and keep it.\n *\n * The guard and the check have to read the same list or the gap comes back, so\n * neither of them owns it.\n *\n * `schema-admin` is here deliberately: it can reach the schema editor and the\n * SQL surfaces, which is administrative by any definition that matters. If a\n * genuinely lesser role is added later, it does not belong in this list — it\n * belongs in a capability check of its own.\n */\nexport const ADMINISTRATIVE_ROLES = [\"admin\", \"schema-admin\"] as const;\n\nexport type AdministrativeRole = (typeof ADMINISTRATIVE_ROLES)[number];\n\n/** Does this single role name carry administrative privilege? */\nexport function isAdministrativeRole(role: string): role is AdministrativeRole {\n return (ADMINISTRATIVE_ROLES as readonly string[]).includes(role);\n}\n\n/** Does any role in this list carry administrative privilege? */\nexport function hasAdministrativeRole(roles: readonly string[] | null | undefined): boolean {\n return !!roles?.some(isAdministrativeRole);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAkBA,IAAa,uBAAuB,CAAC,SAAS,cAAc;;AAK5D,SAAgB,qBAAqB,MAA0C;CAC3E,OAAQ,qBAA2C,SAAS,IAAI;AACpE;;AAGA,SAAgB,sBAAsB,OAAsD;CACxF,OAAO,CAAC,CAAC,OAAO,KAAK,oBAAoB;AAC7C"}
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { createRequire as __rebaseCreateRequire } from "module";
|
|
2
|
-
import __rebaseProcess from "process";
|
|
3
|
-
globalThis.process ??= __rebaseProcess;
|
|
4
|
-
__rebaseCreateRequire(import.meta.url);
|
|
5
|
-
//#region ../types/src/types/admin_block.ts
|
|
6
|
-
/**
|
|
7
|
-
* The keys of a collection's admin block, as data.
|
|
8
|
-
*
|
|
9
|
-
* There is no *type* for the block in this package any more, and that is the point:
|
|
10
|
-
* `admin` is not declared on `BaseCollectionConfig` or on any property here, so a
|
|
11
|
-
* BaaS install cannot even write one. `@rebasepro/cms-types` adds the field back by
|
|
12
|
-
* declaration merging, which is why installing it is what makes the admin surface
|
|
13
|
-
* appear.
|
|
14
|
-
*
|
|
15
|
-
* The *list* still has to live here, because three runtime consumers need it and two
|
|
16
|
-
* of them are core — see below.
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* Every key that belongs inside a collection's `admin` block, as data.
|
|
20
|
-
*
|
|
21
|
-
* The type that describes these fields is `AdminCollectionOptions` in
|
|
22
|
-
* `@rebasepro/cms-types`, and it is erased at build time — but three runtime
|
|
23
|
-
* consumers need the list, and two of them are core:
|
|
24
|
-
*
|
|
25
|
-
* - `serializeCollections`, to drop the block from the contract
|
|
26
|
-
* - the ts-morph schema editor in `@rebasepro/server`, which rewrites collection
|
|
27
|
-
* files on disk from the admin panel and has to know where each key goes. A key
|
|
28
|
-
* missing from this list gets written to the *top level* of the file, where the
|
|
29
|
-
* backend ignores it and the panel never finds it again.
|
|
30
|
-
* - the `collections-admin-block` codemod
|
|
31
|
-
*
|
|
32
|
-
* `@rebasepro/cms-types` re-exports this and asserts it names only real option
|
|
33
|
-
* keys; the count is pinned by a test there.
|
|
34
|
-
*
|
|
35
|
-
* @group Models
|
|
36
|
-
*/
|
|
37
|
-
var ADMIN_COLLECTION_KEYS = [
|
|
38
|
-
"Actions",
|
|
39
|
-
"additionalFields",
|
|
40
|
-
"alwaysApplyDefaultValues",
|
|
41
|
-
"browserCallbacks",
|
|
42
|
-
"components",
|
|
43
|
-
"customViews",
|
|
44
|
-
"defaultEntityAction",
|
|
45
|
-
"defaultFilter",
|
|
46
|
-
"defaultSelectedView",
|
|
47
|
-
"defaultSize",
|
|
48
|
-
"defaultViewMode",
|
|
49
|
-
"disableDefaultActions",
|
|
50
|
-
"display",
|
|
51
|
-
"enabledViews",
|
|
52
|
-
"entityActions",
|
|
53
|
-
"entityViews",
|
|
54
|
-
"exportable",
|
|
55
|
-
"filterPresets",
|
|
56
|
-
"fixedFilter",
|
|
57
|
-
"form",
|
|
58
|
-
"formAutoSave",
|
|
59
|
-
"formView",
|
|
60
|
-
"group",
|
|
61
|
-
"hideFromEntityViews",
|
|
62
|
-
"hideFromNavigation",
|
|
63
|
-
"hideIdFromCollection",
|
|
64
|
-
"hideIdFromForm",
|
|
65
|
-
"icon",
|
|
66
|
-
"includeJsonView",
|
|
67
|
-
"inlineEditing",
|
|
68
|
-
"kanban",
|
|
69
|
-
"listProperties",
|
|
70
|
-
"localChangesBackup",
|
|
71
|
-
"openEntityMode",
|
|
72
|
-
"orderProperty",
|
|
73
|
-
"pagination",
|
|
74
|
-
"previewProperties",
|
|
75
|
-
"propertiesOrder",
|
|
76
|
-
"selectionController",
|
|
77
|
-
"selectionEnabled",
|
|
78
|
-
"sideDialogWidth",
|
|
79
|
-
"sort"
|
|
80
|
-
];
|
|
81
|
-
/**
|
|
82
|
-
* Every key that belongs inside a *property's* `admin` block, as data.
|
|
83
|
-
*
|
|
84
|
-
* The union of `AdminPropertyOptions` and its per-type extensions
|
|
85
|
-
* (`AdminStringOptions`, `AdminArrayOptions`, …) in `@rebasepro/cms-types`.
|
|
86
|
-
* It lives here for the same reason {@link ADMIN_COLLECTION_KEYS} does: the
|
|
87
|
-
* runtime consumers are core packages that the BaaS guard forbids from
|
|
88
|
-
* importing `@rebasepro/cms-types`. Here it is the boot-time collection
|
|
89
|
-
* validator in `@rebasepro/server`, which has to tell "you left `readOnly` at
|
|
90
|
-
* the top of the property, where nothing reads it" apart from "you invented a
|
|
91
|
-
* key we have never heard of".
|
|
92
|
-
*
|
|
93
|
-
* `@rebasepro/cms-types` re-exports this and asserts it names only real
|
|
94
|
-
* option keys.
|
|
95
|
-
*
|
|
96
|
-
* @group Models
|
|
97
|
-
*/
|
|
98
|
-
var ADMIN_PROPERTY_KEYS = [
|
|
99
|
-
"canAddElements",
|
|
100
|
-
"clearable",
|
|
101
|
-
"columnWidth",
|
|
102
|
-
"customProps",
|
|
103
|
-
"disabled",
|
|
104
|
-
"expanded",
|
|
105
|
-
"Field",
|
|
106
|
-
"Filter",
|
|
107
|
-
"filterOperators",
|
|
108
|
-
"fixedFilter",
|
|
109
|
-
"format",
|
|
110
|
-
"hideFromCollection",
|
|
111
|
-
"includeEntityLink",
|
|
112
|
-
"includeId",
|
|
113
|
-
"markdown",
|
|
114
|
-
"minimalistView",
|
|
115
|
-
"multiline",
|
|
116
|
-
"Preview",
|
|
117
|
-
"previewAsTag",
|
|
118
|
-
"previewProperties",
|
|
119
|
-
"readOnly",
|
|
120
|
-
"renderInForm",
|
|
121
|
-
"sortable",
|
|
122
|
-
"span",
|
|
123
|
-
"spreadChildren",
|
|
124
|
-
"urlPreview",
|
|
125
|
-
"widget"
|
|
126
|
-
];
|
|
127
|
-
/**
|
|
128
|
-
* Move flattened admin keys back down into the `admin` block.
|
|
129
|
-
*
|
|
130
|
-
* The admin panel works with a *flat* view model — the block merged onto the
|
|
131
|
-
* collection — so what comes back from a form has `icon` and `defaultViewMode`
|
|
132
|
-
* at the top level while `admin` still holds whatever the file was loaded with.
|
|
133
|
-
* This is the way back.
|
|
134
|
-
*
|
|
135
|
-
* **The top-level value wins.** It is the one the form just wrote; the block is
|
|
136
|
-
* the copy the collection was loaded with, and preferring it resolves every edit
|
|
137
|
-
* in favour of the value the user changed away from.
|
|
138
|
-
*
|
|
139
|
-
* This lives here, next to the key lists, because it had two implementations —
|
|
140
|
-
* `toAdminCollectionConfig` in `@rebasepro/cms-types` and `nestAdminKeys` in
|
|
141
|
-
* `@rebasepro/server`'s schema editor — that agreed on everything except that
|
|
142
|
-
* precedence, which is the only part that decides whether a save is visible.
|
|
143
|
-
*
|
|
144
|
-
* @group Models
|
|
145
|
-
*/
|
|
146
|
-
function nestAdminKeysOf(source, adminKeys) {
|
|
147
|
-
const keys = new Set(adminKeys);
|
|
148
|
-
const top = {};
|
|
149
|
-
const block = { ...source.admin ?? {} };
|
|
150
|
-
for (const [key, value] of Object.entries(source)) {
|
|
151
|
-
if (key === "admin") continue;
|
|
152
|
-
if (keys.has(key)) block[key] = value;
|
|
153
|
-
else top[key] = value;
|
|
154
|
-
}
|
|
155
|
-
if (Object.keys(block).length > 0) top.admin = block;
|
|
156
|
-
return top;
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* {@link nestAdminKeysOf} for a collection.
|
|
160
|
-
*
|
|
161
|
-
* @group Models
|
|
162
|
-
*/
|
|
163
|
-
function nestAdminCollectionKeys(collection) {
|
|
164
|
-
return nestAdminKeysOf(collection, ADMIN_COLLECTION_KEYS);
|
|
165
|
-
}
|
|
166
|
-
/** A record of properties, keyed by name — a map's `properties`, or a `oneOf` block's. */
|
|
167
|
-
function nestEachProperty(properties) {
|
|
168
|
-
return Object.fromEntries(Object.entries(properties).map(([key, child]) => [key, isNestable(child) ? nestAdminPropertyKeys(child) : child]));
|
|
169
|
-
}
|
|
170
|
-
/** Anything the walk can descend into: a plain object, not an array. */
|
|
171
|
-
function isNestable(value) {
|
|
172
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* {@link nestAdminKeysOf} for a property, applied to its children too.
|
|
176
|
-
*
|
|
177
|
-
* A map property carries `properties`, an array property carries `of`, and an
|
|
178
|
-
* array of typed blocks carries `oneOf.properties` — a record of properties like
|
|
179
|
-
* a map's. All of them hold properties with `admin` blocks of their own. A flat
|
|
180
|
-
* `readOnly` left on a child is as dead — and as fatal at the next boot — as one
|
|
181
|
-
* left on the parent, so the walk goes all the way down.
|
|
182
|
-
*
|
|
183
|
-
* `oneOf` was the container this walk did not know about, and it is the one the
|
|
184
|
-
* block-based collection templates are built out of: every block inside them
|
|
185
|
-
* kept its flat `markdown`, and the collection they created would not boot.
|
|
186
|
-
*
|
|
187
|
-
* @group Models
|
|
188
|
-
*/
|
|
189
|
-
function nestAdminPropertyKeys(property) {
|
|
190
|
-
const nested = nestAdminKeysOf(property, ADMIN_PROPERTY_KEYS);
|
|
191
|
-
const children = nested.properties;
|
|
192
|
-
if (isNestable(children)) nested.properties = nestEachProperty(children);
|
|
193
|
-
const oneOf = nested.oneOf;
|
|
194
|
-
if (isNestable(oneOf) && isNestable(oneOf.properties)) nested.oneOf = {
|
|
195
|
-
...oneOf,
|
|
196
|
-
properties: nestEachProperty(oneOf.properties)
|
|
197
|
-
};
|
|
198
|
-
const of = nested.of;
|
|
199
|
-
if (Array.isArray(of)) nested.of = of.map((entry) => isNestable(entry) ? nestAdminPropertyKeys(entry) : entry);
|
|
200
|
-
else if (isNestable(of)) nested.of = nestAdminPropertyKeys(of);
|
|
201
|
-
return nested;
|
|
202
|
-
}
|
|
203
|
-
//#endregion
|
|
204
|
-
export { nestAdminPropertyKeys as i, ADMIN_PROPERTY_KEYS as n, nestAdminCollectionKeys as r, ADMIN_COLLECTION_KEYS as t };
|
|
205
|
-
|
|
206
|
-
//# sourceMappingURL=admin_block-DxKLmdiv.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"admin_block-DxKLmdiv.js","names":[],"sources":["../../types/src/types/admin_block.ts"],"sourcesContent":["/**\n * The keys of a collection's admin block, as data.\n *\n * There is no *type* for the block in this package any more, and that is the point:\n * `admin` is not declared on `BaseCollectionConfig` or on any property here, so a\n * BaaS install cannot even write one. `@rebasepro/cms-types` adds the field back by\n * declaration merging, which is why installing it is what makes the admin surface\n * appear.\n *\n * The *list* still has to live here, because three runtime consumers need it and two\n * of them are core — see below.\n */\n\n/**\n * Every key that belongs inside a collection's `admin` block, as data.\n *\n * The type that describes these fields is `AdminCollectionOptions` in\n * `@rebasepro/cms-types`, and it is erased at build time — but three runtime\n * consumers need the list, and two of them are core:\n *\n * - `serializeCollections`, to drop the block from the contract\n * - the ts-morph schema editor in `@rebasepro/server`, which rewrites collection\n * files on disk from the admin panel and has to know where each key goes. A key\n * missing from this list gets written to the *top level* of the file, where the\n * backend ignores it and the panel never finds it again.\n * - the `collections-admin-block` codemod\n *\n * `@rebasepro/cms-types` re-exports this and asserts it names only real option\n * keys; the count is pinned by a test there.\n *\n * @group Models\n */\nexport const ADMIN_COLLECTION_KEYS = [\n \"Actions\",\n \"additionalFields\",\n \"alwaysApplyDefaultValues\",\n \"browserCallbacks\",\n \"components\",\n \"customViews\",\n \"defaultEntityAction\",\n \"defaultFilter\",\n \"defaultSelectedView\",\n \"defaultSize\",\n \"defaultViewMode\",\n \"disableDefaultActions\",\n \"display\",\n \"enabledViews\",\n \"entityActions\",\n \"entityViews\",\n \"exportable\",\n \"filterPresets\",\n \"fixedFilter\",\n \"form\",\n \"formAutoSave\",\n \"formView\",\n \"group\",\n \"hideFromEntityViews\",\n \"hideFromNavigation\",\n \"hideIdFromCollection\",\n \"hideIdFromForm\",\n \"icon\",\n \"includeJsonView\",\n \"inlineEditing\",\n \"kanban\",\n \"listProperties\",\n \"localChangesBackup\",\n \"openEntityMode\",\n \"orderProperty\",\n \"pagination\",\n \"previewProperties\",\n \"propertiesOrder\",\n \"selectionController\",\n \"selectionEnabled\",\n \"sideDialogWidth\",\n \"sort\"\n] as const;\n\n/** A key of a collection's `admin` block. @group Models */\nexport type AdminCollectionKey = typeof ADMIN_COLLECTION_KEYS[number];\n\n/**\n * Every key that belongs inside a *property's* `admin` block, as data.\n *\n * The union of `AdminPropertyOptions` and its per-type extensions\n * (`AdminStringOptions`, `AdminArrayOptions`, …) in `@rebasepro/cms-types`.\n * It lives here for the same reason {@link ADMIN_COLLECTION_KEYS} does: the\n * runtime consumers are core packages that the BaaS guard forbids from\n * importing `@rebasepro/cms-types`. Here it is the boot-time collection\n * validator in `@rebasepro/server`, which has to tell \"you left `readOnly` at\n * the top of the property, where nothing reads it\" apart from \"you invented a\n * key we have never heard of\".\n *\n * `@rebasepro/cms-types` re-exports this and asserts it names only real\n * option keys.\n *\n * @group Models\n */\nexport const ADMIN_PROPERTY_KEYS = [\n \"canAddElements\",\n \"clearable\",\n \"columnWidth\",\n \"customProps\",\n \"disabled\",\n \"expanded\",\n \"Field\",\n \"Filter\",\n \"filterOperators\",\n \"fixedFilter\",\n \"format\",\n \"hideFromCollection\",\n \"includeEntityLink\",\n \"includeId\",\n \"markdown\",\n \"minimalistView\",\n \"multiline\",\n \"Preview\",\n \"previewAsTag\",\n \"previewProperties\",\n \"readOnly\",\n \"renderInForm\",\n \"sortable\",\n \"span\",\n \"spreadChildren\",\n \"urlPreview\",\n \"widget\",\n] as const;\n\n/** A key of a property's `admin` block. @group Models */\nexport type AdminPropertyKey = typeof ADMIN_PROPERTY_KEYS[number];\n\n/**\n * Move flattened admin keys back down into the `admin` block.\n *\n * The admin panel works with a *flat* view model — the block merged onto the\n * collection — so what comes back from a form has `icon` and `defaultViewMode`\n * at the top level while `admin` still holds whatever the file was loaded with.\n * This is the way back.\n *\n * **The top-level value wins.** It is the one the form just wrote; the block is\n * the copy the collection was loaded with, and preferring it resolves every edit\n * in favour of the value the user changed away from.\n *\n * This lives here, next to the key lists, because it had two implementations —\n * `toAdminCollectionConfig` in `@rebasepro/cms-types` and `nestAdminKeys` in\n * `@rebasepro/server`'s schema editor — that agreed on everything except that\n * precedence, which is the only part that decides whether a save is visible.\n *\n * @group Models\n */\nexport function nestAdminKeysOf(\n source: Record<string, unknown>,\n adminKeys: readonly string[]\n): Record<string, unknown> {\n const keys = new Set<string>(adminKeys);\n const top: Record<string, unknown> = {};\n const block: Record<string, unknown> = { ...((source.admin as Record<string, unknown> | undefined) ?? {}) };\n\n for (const [key, value] of Object.entries(source)) {\n if (key === \"admin\") continue;\n if (keys.has(key)) block[key] = value;\n else top[key] = value;\n }\n\n if (Object.keys(block).length > 0) top.admin = block;\n return top;\n}\n\n/**\n * {@link nestAdminKeysOf} for a collection.\n *\n * @group Models\n */\nexport function nestAdminCollectionKeys(collection: Record<string, unknown>): Record<string, unknown> {\n return nestAdminKeysOf(collection, ADMIN_COLLECTION_KEYS);\n}\n\n/** A record of properties, keyed by name — a map's `properties`, or a `oneOf` block's. */\nfunction nestEachProperty(properties: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(\n Object.entries(properties).map(([key, child]) => [\n key,\n isNestable(child) ? nestAdminPropertyKeys(child) : child\n ])\n );\n}\n\n/** Anything the walk can descend into: a plain object, not an array. */\nfunction isNestable(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * {@link nestAdminKeysOf} for a property, applied to its children too.\n *\n * A map property carries `properties`, an array property carries `of`, and an\n * array of typed blocks carries `oneOf.properties` — a record of properties like\n * a map's. All of them hold properties with `admin` blocks of their own. A flat\n * `readOnly` left on a child is as dead — and as fatal at the next boot — as one\n * left on the parent, so the walk goes all the way down.\n *\n * `oneOf` was the container this walk did not know about, and it is the one the\n * block-based collection templates are built out of: every block inside them\n * kept its flat `markdown`, and the collection they created would not boot.\n *\n * @group Models\n */\nexport function nestAdminPropertyKeys(property: Record<string, unknown>): Record<string, unknown> {\n const nested = nestAdminKeysOf(property, ADMIN_PROPERTY_KEYS);\n\n const children = nested.properties;\n if (isNestable(children)) {\n nested.properties = nestEachProperty(children);\n }\n\n // `oneOf` is not itself a property — it is a block holding `properties`\n // alongside `typeField`, `valueField` and `propertiesOrder`, none of which\n // may be walked as one.\n const oneOf = nested.oneOf;\n if (isNestable(oneOf) && isNestable(oneOf.properties)) {\n nested.oneOf = { ...oneOf, properties: nestEachProperty(oneOf.properties) };\n }\n\n const of = nested.of;\n if (Array.isArray(of)) {\n nested.of = of.map(entry => isNestable(entry) ? nestAdminPropertyKeys(entry) : entry);\n } else if (isNestable(of)) {\n nested.of = nestAdminPropertyKeys(of);\n }\n\n return nested;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,wBAAwB;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;;;;;;;;;;;;;;;;;AAsBA,IAAa,sBAAsB;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,gBACZ,QACA,WACuB;CACvB,MAAM,OAAO,IAAI,IAAY,SAAS;CACtC,MAAM,MAA+B,CAAC;CACtC,MAAM,QAAiC,EAAE,GAAK,OAAO,SAAiD,CAAC,EAAG;CAE1G,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;EAC/C,IAAI,QAAQ,SAAS;EACrB,IAAI,KAAK,IAAI,GAAG,GAAG,MAAM,OAAO;OAC3B,IAAI,OAAO;CACpB;CAEA,IAAI,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,GAAG,IAAI,QAAQ;CAC/C,OAAO;AACX;;;;;;AAOA,SAAgB,wBAAwB,YAA8D;CAClG,OAAO,gBAAgB,YAAY,qBAAqB;AAC5D;;AAGA,SAAS,iBAAiB,YAA8D;CACpF,OAAO,OAAO,YACV,OAAO,QAAQ,UAAU,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CAC7C,KACA,WAAW,KAAK,IAAI,sBAAsB,KAAK,IAAI,KACvD,CAAC,CACL;AACJ;;AAGA,SAAS,WAAW,OAAkD;CAClE,OAAO,QAAQ,KAAK,KAAK,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AAC9E;;;;;;;;;;;;;;;;AAiBA,SAAgB,sBAAsB,UAA4D;CAC9F,MAAM,SAAS,gBAAgB,UAAU,mBAAmB;CAE5D,MAAM,WAAW,OAAO;CACxB,IAAI,WAAW,QAAQ,GACnB,OAAO,aAAa,iBAAiB,QAAQ;CAMjD,MAAM,QAAQ,OAAO;CACrB,IAAI,WAAW,KAAK,KAAK,WAAW,MAAM,UAAU,GAChD,OAAO,QAAQ;EAAE,GAAG;EAAO,YAAY,iBAAiB,MAAM,UAAU;CAAE;CAG9E,MAAM,KAAK,OAAO;CAClB,IAAI,MAAM,QAAQ,EAAE,GAChB,OAAO,KAAK,GAAG,KAAI,UAAS,WAAW,KAAK,IAAI,sBAAsB,KAAK,IAAI,KAAK;MACjF,IAAI,WAAW,EAAE,GACpB,OAAO,KAAK,sBAAsB,EAAE;CAGxC,OAAO;AACX"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ast-schema-editor-CslO8Oje.js","names":[],"sources":["../src/api/ast-schema-editor.ts"],"sourcesContent":["import { Project, SyntaxKind, Node, ObjectLiteralExpression, ObjectLiteralElementLike, PropertyAssignment, SourceFile, IndentationText } from \"ts-morph\";\nimport { nestAdminCollectionKeys, nestAdminPropertyKeys } from \"@rebasepro/types\";\nimport * as path from \"path\";\nimport * as fs from \"fs\";\n\n/**\n * The helpers a collection file may be wrapped in.\n *\n * `rebase init` scaffolds every collection as\n * `const postsCollection = defineCollection({ … })` — a call expression, not the\n * bare object literal `rebase introspect` emits. An editor that only understood\n * the bare form found nothing to patch in any stock project, and then rewrote the\n * file from the panel's JSON: no wrapper, no imports, no relation thunks.\n */\nconst COLLECTION_FACTORIES = new Set([\"defineCollection\"]);\n\n/**\n * The only relation-target expression this editor will write through verbatim.\n *\n * A relation's `target` is emitted as SOURCE, not as a string literal, because\n * it has to be `() => otherCollection`. The test for \"is this already a thunk?\"\n * used to be \"does it contain an arrow\", which `() => { require(\"child_process\")\n * .execSync(\"…\") }` also satisfies — and `rebase dev` re-imports the file the\n * moment it changes, so the payload ran without anyone deploying anything.\n *\n * An arrow returning a single identifier is the whole grammar. Anything else is\n * read as a collection name and turned into a thunk by `targetThunk`, which\n * resolves it against the collections directory and refuses a name with no file.\n */\nconst ARROW_TO_IDENTIFIER = /^\\(\\s*\\)\\s*=>\\s*[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * A value that must be emitted as source code rather than as JSON.\n *\n * Everything reaching the writer has been through `JSON.stringify` on the wire,\n * so a function-valued key arrives either missing or as a string. A relation's\n * `target` is a thunk in the file and a slug in the payload; this is how the\n * thunk gets written back.\n */\nclass RawExpression {\n constructor(public readonly text: string) {\n }\n}\n\n/**\n * Move presentation keys into the `admin` block.\n *\n * The rule itself lives in `@rebasepro/types`, next to `ADMIN_COLLECTION_KEYS`,\n * because `@rebasepro/cms-types` has to apply the identical one on the panel's\n * side and this package cannot import that one. Two copies used to exist and\n * they disagreed about precedence, which decided whether a presentation edit was\n * saved or silently reverted to the value the user had just changed away from.\n */\nexport function nestAdminKeys(collectionData: Record<string, unknown>): Record<string, unknown> {\n return nestAdminCollectionKeys(collectionData);\n}\n\n/**\n * {@link nestAdminKeys} for a whole collection — its properties included.\n *\n * `nestAdminKeys` only ever moved the COLLECTION's presentation keys. A\n * property's — `markdown`, `multiline`, `previewAsTag` — went to disk exactly as\n * the panel sent them, at the top level of the property, which the boot\n * validator treats as fatal. `saveProperty` nests them; `saveCollection` did\n * not, and *creating* a collection is a `saveCollection`.\n *\n * So a collection created from a template wrote a file that could not boot, and\n * took the whole project down with it — the loader imports every collection in\n * the directory, so one bad file stops `rebase dev` from starting at all. The\n * panel reported success.\n */\nexport function nestAdminKeysDeep(collectionData: Record<string, unknown>): Record<string, unknown> {\n const nested = nestAdminKeys(collectionData);\n const properties = nested.properties;\n if (properties && typeof properties === \"object\" && !Array.isArray(properties)) {\n nested.properties = Object.fromEntries(\n Object.entries(properties as Record<string, unknown>).map(([key, property]) => [\n key,\n property && typeof property === \"object\" && !Array.isArray(property)\n ? nestAdminPropertyKeys(property as Record<string, unknown>)\n : property\n ])\n );\n }\n return nested;\n}\n\nexport class AstSchemaEditor {\n private project: Project;\n private collectionsDir: string;\n\n constructor(collectionsDir: string) {\n this.project = new Project({\n manipulationSettings: {\n indentationText: IndentationText.FourSpaces\n }\n });\n if (fs.existsSync(collectionsDir)) {\n this.project.addSourceFilesAtPaths(`${collectionsDir}/**/*.ts`);\n }\n this.collectionsDir = path.resolve(collectionsDir);\n }\n\n /**\n * Sanitize collectionId to prevent path traversal attacks.\n * Only allows alphanumeric characters, underscores, and hyphens.\n */\n /**\n * The variable name the generated file binds the collection to.\n *\n * `sanitizeCollectionId` guards the FILENAME, and permits hyphens and a\n * leading digit — both legal in a filename and neither legal in a\n * JavaScript identifier. So a collection created from the admin panel as\n * `my-notes` (the documented slug shape) or `2024 Archive` (auto-slugged to\n * `2024_archive`) wrote:\n *\n * const my-notesCollection: CollectionConfig = … \"',' expected\"\n * const 2024_archiveCollection: CollectionConfig = … \"Numeric separators\n * are not allowed here\"\n *\n * The panel reported success, and the next boot failed for EVERY collection\n * in the directory — the loader imports all of them — while the editor could\n * no longer parse the file it had just written, so it could not fix itself.\n *\n * Separators camel-case rather than vanish, so `my-notes` and `my_notes`\n * stay distinct; a leading digit is prefixed rather than stripped, so\n * `2024_archive` stays distinct from `archive`. Byte-identical for every\n * slug that already produced a valid identifier.\n */\n private static collectionVarName(safeId: string): string {\n const camel = safeId.replace(/[-_]+([a-zA-Z0-9])/g, (_, char: string) => char.toUpperCase());\n const identifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(camel) ? camel : `c${camel.replace(/^[0-9]/, (d) => d)}`;\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier) ? identifier : `c${identifier.replace(/[^A-Za-z0-9_$]/g, \"\")}`;\n }\n\n private sanitizeCollectionId(collectionId: string): string {\n const sanitized = collectionId.replace(/[^a-zA-Z0-9_-]/g, \"\");\n if (!sanitized || sanitized !== collectionId) {\n throw new Error(`Invalid collection ID: \"${collectionId}\". Only alphanumeric characters, underscores, and hyphens are allowed.`);\n }\n return sanitized;\n }\n\n /**\n * Resolve a file path and ensure it falls within the collectionsDir.\n */\n private safePath(filename: string): string {\n const resolved = path.resolve(this.collectionsDir, filename);\n if (!resolved.startsWith(this.collectionsDir + path.sep) && resolved !== this.collectionsDir) {\n throw new Error(\"Path traversal detected: resolved path is outside the collections directory.\");\n }\n return resolved;\n }\n\n private getCollectionFile(collectionId: string) {\n const safeId = this.sanitizeCollectionId(collectionId);\n const filePath = this.safePath(`${safeId}.ts`);\n let file = this.project.getSourceFile(filePath);\n if (!file && fs.existsSync(filePath)) {\n this.project.addSourceFilesAtPaths(`${this.collectionsDir}/**/*.ts`);\n file = this.project.getSourceFile(filePath);\n }\n return file;\n }\n\n /**\n * Find the object literal a collection is declared with, through whatever\n * wraps it.\n *\n * `defineCollection({ … })` is the shape `rebase init` writes and the one the\n * docs recommend; `satisfies` / `as` / parentheses are the other ways an\n * author can dress the same literal. Returning `null` for any of them meant\n * the three callers below each failed differently, and the worst of them\n * overwrote the file.\n */\n private unwrapCollectionObject(node: Node | undefined): ObjectLiteralExpression | null {\n if (!node) return null;\n if (Node.isObjectLiteralExpression(node)) return node;\n if (Node.isParenthesizedExpression(node) ||\n Node.isAsExpression(node) ||\n Node.isSatisfiesExpression(node) ||\n Node.isTypeAssertion(node) ||\n Node.isNonNullExpression(node)) {\n return this.unwrapCollectionObject(node.getExpression());\n }\n if (Node.isCallExpression(node)) {\n // `defineCollection`, `admin.defineCollection`, `defineCollection<Post>`\n const callee = node.getExpression().getText().split(\".\").pop();\n if (callee && COLLECTION_FACTORIES.has(callee)) {\n return this.unwrapCollectionObject(node.getArguments()[0]);\n }\n }\n return null;\n }\n\n private getCollectionObject(collectionId: string): ObjectLiteralExpression | null {\n const file = this.getCollectionFile(collectionId);\n if (!file) return null;\n\n const defaultExport = file.getDefaultExportSymbol();\n if (defaultExport) {\n const declaration = defaultExport.getDeclarations()[0];\n if (declaration && declaration.getKind() === SyntaxKind.ExportAssignment) {\n const expr = declaration.asKind(SyntaxKind.ExportAssignment)?.getExpression();\n if (expr && expr.getKind() === SyntaxKind.Identifier) {\n const varName = expr.getText();\n const varDecl = file.getVariableDeclaration(varName);\n const unwrapped = this.unwrapCollectionObject(varDecl?.getInitializer());\n if (unwrapped) return unwrapped;\n } else {\n // `export default defineCollection({ … })`\n const unwrapped = this.unwrapCollectionObject(expr);\n if (unwrapped) return unwrapped;\n }\n }\n }\n // Fallback: the first VariableDeclaration that holds a collection literal\n for (const varDecl of file.getVariableDeclarations()) {\n const init = this.unwrapCollectionObject(varDecl.getInitializer());\n if (init) return init;\n }\n return null;\n }\n\n /**\n * The collection's object literal, or a refusal that says what to do.\n *\n * Every caller needs this to be all-or-nothing: a missing object literal used\n * to mean \"throw\", \"report success and do nothing\" and \"recreate the file\n * from scratch\" depending on which method you called.\n */\n private requireCollectionObject(collectionId: string): ObjectLiteralExpression {\n const file = this.getCollectionFile(collectionId);\n if (!file) {\n throw new Error(`Collection \"${collectionId}\" has no file at ${path.join(this.collectionsDir, `${collectionId}.ts`)}.`);\n }\n const collectionObj = this.getCollectionObject(collectionId);\n if (!collectionObj) {\n throw new Error(this.unreadableFileMessage(collectionId, file));\n }\n return collectionObj;\n }\n\n private unreadableFileMessage(collectionId: string, file: SourceFile): string {\n return `Could not find the collection object in ${file.getFilePath()}. ` +\n \"The schema editor can only edit a collection declared as `const x = defineCollection({ … })` \" +\n \"or `const x: CollectionConfig = { … }` and exported as the file's default. \" +\n `Edit \"${collectionId}\" in code instead.`;\n }\n\n /** Look a key up on an object literal, quoted or not. */\n private findProperty(obj: ObjectLiteralExpression, name: string): ObjectLiteralElementLike | undefined {\n return obj.getProperty((p: ObjectLiteralElementLike) =>\n \"getName\" in p &&\n typeof (p as PropertyAssignment).getName === \"function\" &&\n ((p as PropertyAssignment).getName() === name ||\n (p as PropertyAssignment).getName() === `\"${name}\"` ||\n (p as PropertyAssignment).getName() === `'${name}'`));\n }\n\n private static quoteKey(key: string): string {\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);\n }\n\n private static isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value) && !(value instanceof RawExpression);\n }\n\n private convertJsonToAstString(obj: unknown, indentLevel = 0, oldAstNode?: ObjectLiteralExpression): string {\n // Base TS-morph parses arrays as 2 levels deep from the property key:\n // PropertiesObject = level 1, PropertyConfig = level 2.\n // We calibrate the spacing multiples to keep the items flush with standard TS format.\n const indentStr = \" \";\n const indent = indentStr.repeat(indentLevel);\n const innerIndent = indentStr.repeat(indentLevel + 1);\n\n if (obj instanceof RawExpression) {\n return obj.text;\n }\n if (obj === null || obj === undefined) {\n return \"undefined\";\n }\n if (typeof obj === \"string\") {\n return JSON.stringify(obj);\n }\n if (typeof obj === \"number\" || typeof obj === \"boolean\") {\n return String(obj);\n }\n if (Array.isArray(obj)) {\n if (obj.length === 0) return \"[]\";\n const items = obj.map(item => this.convertJsonToAstString(item, indentLevel + 1));\n return `[\\n${innerIndent}${items.join(`,\\n${innerIndent}`)}\\n${indent}]`;\n }\n if (typeof obj === \"object\") {\n const record = obj as Record<string, unknown>;\n const keys = Object.keys(record);\n\n // Collect preserved AST properties\n const preservedProps: string[] = [];\n if (oldAstNode) {\n const oldProps = oldAstNode.getProperties();\n for (const oldProp of oldProps) {\n if (oldProp.isKind(SyntaxKind.PropertyAssignment)) {\n const nameNode = oldProp.getNameNode();\n let name = nameNode.getText();\n if (name.startsWith('\"') && name.endsWith('\"')) name = name.slice(1, -1);\n if (name.startsWith(\"'\") && name.endsWith(\"'\")) name = name.slice(1, -1);\n\n // If the JSON object doesn't have this key, check if we should preserve it\n if (!(name in record)) {\n const init = oldProp.getInitializer();\n if (init) {\n const kind = init.getKind();\n const isCode = kind === SyntaxKind.ArrowFunction ||\n kind === SyntaxKind.FunctionExpression ||\n kind === SyntaxKind.Identifier ||\n kind === SyntaxKind.CallExpression ||\n kind === SyntaxKind.JsxElement;\n\n if (isCode || name === \"target\" || name === \"callbacks\" || name === \"browserCallbacks\" || name === \"permissions\" || name === \"securityRules\") {\n // Preserve this property exactly as it was\n preservedProps.push(`${AstSchemaEditor.quoteKey(name)}: ${init.getText()}`);\n }\n }\n }\n }\n }\n }\n\n if (keys.length === 0 && preservedProps.length === 0) return \"{}\";\n\n const props = keys.map(key => {\n const keyStr = AstSchemaEditor.quoteKey(key);\n\n // If the value is an object, pass the old AST node to recurse\n let childAstNode: ObjectLiteralExpression | undefined;\n if (oldAstNode && AstSchemaEditor.isPlainObject(record[key])) {\n const oldProp = this.findProperty(oldAstNode, key);\n if (oldProp && oldProp.isKind(SyntaxKind.PropertyAssignment)) {\n childAstNode = oldProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n }\n }\n\n return `${keyStr}: ${this.convertJsonToAstString(record[key], indentLevel + 1, childAstNode)}`;\n });\n\n const allProps = [...props, ...preservedProps];\n return `{\\n${innerIndent}${allProps.join(`,\\n${innerIndent}`)}\\n${indent}}`;\n }\n return \"undefined\";\n }\n\n /**\n * Write only the keys the patch names, leaving every sibling alone.\n *\n * A patch says what changed, not what the collection is. The panel sends one\n * — `{ propertiesOrder }` is what adding a column posts — and rewriting the\n * `admin` block from it deleted the collection's icon, group, list columns\n * and kanban config in the same write.\n */\n private mergeIntoObjectLiteral(target: ObjectLiteralExpression, data: Record<string, unknown>, indentLevel: number): void {\n for (const [key, value] of Object.entries(data)) {\n const existing = this.findProperty(target, key);\n const existingObj = existing && existing.isKind(SyntaxKind.PropertyAssignment)\n ? existing.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression)\n : undefined;\n\n if (existingObj && AstSchemaEditor.isPlainObject(value)) {\n this.mergeIntoObjectLiteral(existingObj, value, indentLevel + 1);\n continue;\n }\n\n const initializer = this.convertJsonToAstString(value, indentLevel, existingObj);\n if (existing && existing.isKind(SyntaxKind.PropertyAssignment)) {\n existing.setInitializer(initializer);\n } else {\n target.addPropertyAssignment({\n name: AstSchemaEditor.quoteKey(key),\n initializer\n });\n }\n }\n }\n\n public async saveProperty(collectionId: string, propertyKey: string, propertyConfig: Record<string, unknown>) {\n const collectionObj = this.requireCollectionObject(collectionId);\n\n // The panel's property forms bind to the flat names — `readOnly`,\n // `hideFromCollection` — while on disk they belong inside the property's\n // own `admin` block. Written flat they are not merely ignored: the boot\n // validator treats a moved key as fatal.\n const nestedConfig = nestAdminPropertyKeys(propertyConfig);\n\n let propertiesProp = collectionObj.getProperty(\"properties\") as PropertyAssignment;\n if (!propertiesProp) {\n propertiesProp = collectionObj.addPropertyAssignment({\n name: \"properties\",\n initializer: \"{}\"\n });\n }\n\n const propsObj = propertiesProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n if (propsObj) {\n const existingProp = this.findProperty(propsObj, propertyKey);\n\n let oldPropAstNode: ObjectLiteralExpression | undefined;\n if (existingProp && existingProp.isKind(SyntaxKind.PropertyAssignment)) {\n oldPropAstNode = existingProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n }\n\n const newInitializer = this.convertJsonToAstString(nestedConfig, 2, oldPropAstNode);\n\n if (existingProp) {\n if (existingProp.isKind(SyntaxKind.PropertyAssignment)) {\n existingProp.setInitializer(newInitializer);\n }\n } else {\n propsObj.addPropertyAssignment({\n name: AstSchemaEditor.quoteKey(propertyKey),\n initializer: newInitializer\n });\n }\n\n const file = this.getCollectionFile(collectionId);\n if (file) {\n file.formatText();\n }\n await this.project.save();\n }\n }\n\n public async deleteProperty(collectionId: string, propertyKey: string) {\n const collectionObj = this.requireCollectionObject(collectionId);\n\n const propertiesProp = collectionObj.getProperty(\"properties\") as PropertyAssignment;\n if (propertiesProp) {\n const propsObj = propertiesProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n if (propsObj) {\n const existingProp = this.findProperty(propsObj, propertyKey);\n if (existingProp) {\n existingProp.remove();\n const file = this.getCollectionFile(collectionId);\n if (file) {\n file.formatText();\n }\n await this.project.save();\n }\n }\n }\n }\n\n /**\n * Write a collection back to its file.\n *\n * `partial` is the difference between \"this is the collection\" and \"this is\n * what changed about it\". The panel sends both — a full save from the editor\n * dialog, and a one-key patch whenever a property is added, deleted or\n * reordered — and they cannot be told apart by looking at the payload. Read\n * as a full save, a patch deletes everything it does not mention, including\n * `securityRules`; the loader then hands the collection the directory\n * default, which in the scaffold is `access: \"public\"`.\n */\n public async saveCollection(collectionId: string, collectionData: Record<string, unknown>, options: { partial?: boolean } = {}) {\n const partial = options.partial === true;\n let file = this.getCollectionFile(collectionId);\n const collectionObj = file ? this.getCollectionObject(collectionId) : null;\n\n if (file && !collectionObj) {\n // The file is there and we could not read it. Recreating it from the\n // panel's JSON would drop the imports, the callbacks and the relation\n // thunks that JSON cannot carry.\n throw new Error(this.unreadableFileMessage(collectionId, file));\n }\n\n if (!file || !collectionObj) {\n if (partial) {\n throw new Error(`Cannot apply a partial update to \"${collectionId}\": it has no collection file yet.`);\n }\n // Create a new file\n const safeId = this.sanitizeCollectionId(collectionId);\n const newFilePath = this.safePath(`${safeId}.ts`);\n if (fs.existsSync(newFilePath)) {\n throw new Error(`Refusing to overwrite ${newFilePath}: a file for \"${collectionId}\" already exists but could not be parsed.`);\n }\n const varName = `${AstSchemaEditor.collectionVarName(safeId)}Collection`;\n file = this.project.createSourceFile(newFilePath, `import { CollectionConfig } from \"@rebasepro/types\";\\n\\nconst ${varName}: CollectionConfig = ${this.convertJsonToAstString(nestAdminKeysDeep(collectionData))};\\n\\nexport default ${varName};\\n`);\n } else {\n // Update root level properties gracefully\n\n if (!partial) {\n // Force delete securityRules if empty or undefined to handle Formex / serialization stripping\n if (!(\"securityRules\" in collectionData) || collectionData.securityRules === undefined || (Array.isArray(collectionData.securityRules) && collectionData.securityRules.length === 0)) {\n const srProp = collectionObj.getProperty(\"securityRules\");\n if (srProp) {\n srProp.remove();\n }\n\n // If it was in collectionData as an empty array, delete it so the loop below doesn't add it back as \"[]\"\n // Actually, if it's \"[]\", omitting it entirely from the TS file achieves the same logical effect (no RLS rules)\n // and correctly triggers \"unmapped policies\" if the DB still has them.\n delete collectionData[\"securityRules\"];\n }\n }\n\n // The panel works with a flat view model — presentation merged onto the\n // collection — so what arrives here has `icon` and `listProperties` at\n // the top level. On disk they belong inside `admin`. Writing them flat\n // would produce a file the backend loads and ignores and the panel\n // never reads back, which looks exactly like the edit not saving. A\n // property's presentation keys are worse than ignored: they are fatal\n // at the next boot, which is why this goes all the way down.\n collectionData = nestAdminKeysDeep(collectionData);\n\n for (const key of Object.keys(collectionData)) {\n if (key === \"relations\") {\n this.writeRelations(collectionId, file, collectionObj, collectionData[key]);\n continue;\n }\n\n const prop = collectionObj.getProperty(key) as PropertyAssignment;\n\n let oldAstNode: ObjectLiteralExpression | undefined;\n if (prop && prop.isKind(SyntaxKind.PropertyAssignment)) {\n oldAstNode = prop.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n }\n\n if (partial && oldAstNode && AstSchemaEditor.isPlainObject(collectionData[key])) {\n this.mergeIntoObjectLiteral(oldAstNode, collectionData[key] as Record<string, unknown>, 2);\n continue;\n }\n\n const newInit = this.convertJsonToAstString(collectionData[key], 1, oldAstNode);\n if (prop) {\n prop.setInitializer(newInit);\n } else {\n collectionObj.addPropertyAssignment({\n // `quoteKey`, like every other site that emits a key.\n // This one did not, and ts-morph writes a property name\n // out verbatim — so a top-level key of\n // `injected: (() => { … })(), tail` closed the property\n // and opened an expression, which `rebase dev` then\n // re-imported and ran. The rest of this file has always\n // quoted; this was the one that did not.\n name: AstSchemaEditor.quoteKey(key),\n initializer: newInit\n });\n }\n }\n }\n if (file) {\n file.formatText();\n }\n await this.project.save();\n }\n\n /**\n * Write the collection-level `relations` array.\n *\n * Relations are the one key whose values are not all data: `target` is a\n * thunk in the file, and the panel sends the target collection's slug (or,\n * for a relation it did not touch, nothing at all — `JSON.stringify` drops\n * the function). So each entry's target is resolved in that order: a slug\n * becomes `() => xCollection` plus the import it needs, and anything else\n * falls back to the thunk already in the file.\n *\n * This used to be a `continue` with a comment saying relations were handled\n * elsewhere. Nothing handled them; every edit in the Relations tab was\n * dropped without a word.\n */\n private writeRelations(collectionId: string, file: SourceFile, collectionObj: ObjectLiteralExpression, value: unknown): void {\n if (!Array.isArray(value)) return;\n\n const existing = this.findProperty(collectionObj, \"relations\");\n const oldArray = existing && existing.isKind(SyntaxKind.PropertyAssignment)\n ? existing.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)\n : undefined;\n\n const oldElements = oldArray?.getElements() ?? [];\n const oldTargetsByName = new Map<string, string>();\n // Only for entries the file left unnamed — `relationName` is optional, and\n // those cannot be matched any other way. Positional matching is off the\n // moment the array's length changes, because then a position no longer\n // means the same relation.\n const anonymousTargetsByIndex: (string | undefined)[] = [];\n oldElements.forEach((element, index) => {\n const elementObj = element.asKind(SyntaxKind.ObjectLiteralExpression);\n if (!elementObj) return;\n const targetProp = this.findProperty(elementObj, \"target\");\n const targetInit = targetProp && targetProp.isKind(SyntaxKind.PropertyAssignment)\n ? targetProp.getInitializer()\n : undefined;\n if (!targetInit) return;\n\n const nameProp = this.findProperty(elementObj, \"relationName\");\n const nameInit = nameProp && nameProp.isKind(SyntaxKind.PropertyAssignment)\n ? nameProp.getInitializerIfKind(SyntaxKind.StringLiteral)\n : undefined;\n if (nameInit) oldTargetsByName.set(nameInit.getLiteralValue(), targetInit.getText());\n else anonymousTargetsByIndex[index] = targetInit.getText();\n });\n\n const items = value.map((entry, index) => {\n if (!AstSchemaEditor.isPlainObject(entry)) return entry;\n const relation: Record<string, unknown> = { ...entry };\n const relationName = typeof relation.relationName === \"string\" ? relation.relationName : undefined;\n const rawTarget = relation.target;\n\n let targetText: string | undefined;\n if (typeof rawTarget === \"string\" && rawTarget.trim().length > 0) {\n const trimmed = rawTarget.trim();\n // A target that already looks like a thunk is written into the\n // file as source, so \"contains an arrow\" is not a good enough\n // reason to trust it: `() => { require(\"child_process\")… }` also\n // contains one. Only the exact shape this emits is accepted —\n // an arrow returning one identifier — and anything else is\n // treated as a collection NAME and turned into a thunk here,\n // which is the path that was always safe.\n targetText = ARROW_TO_IDENTIFIER.test(trimmed)\n ? trimmed\n : this.targetThunk(file, trimmed);\n } else if (relationName && oldTargetsByName.has(relationName)) {\n targetText = oldTargetsByName.get(relationName);\n } else if (value.length === oldElements.length) {\n targetText = anonymousTargetsByIndex[index];\n }\n\n if (!targetText) {\n throw new Error(`Relation \"${relationName ?? `#${index}`}\" on collection \"${collectionId}\" has no target collection. Pick one before saving.`);\n }\n\n relation.target = new RawExpression(targetText);\n return relation;\n });\n\n const initializer = this.convertJsonToAstString(items, 1);\n if (existing && existing.isKind(SyntaxKind.PropertyAssignment)) {\n existing.setInitializer(initializer);\n } else {\n collectionObj.addPropertyAssignment({ name: \"relations\", initializer });\n }\n }\n\n /**\n * `() => targetCollection` for a target named by its slug, importing it if\n * the file does not already.\n */\n private targetThunk(file: SourceFile, targetSlug: string): string {\n const targetFile = this.getCollectionFile(targetSlug);\n if (!targetFile) {\n throw new Error(`Cannot link to collection \"${targetSlug}\": no file for it in ${this.collectionsDir}.`);\n }\n\n const identifier = this.getDefaultExportName(targetFile);\n if (!identifier) {\n throw new Error(`Cannot link to collection \"${targetSlug}\": ${targetFile.getFilePath()} has no default export to import.`);\n }\n\n if (targetFile.getFilePath() !== file.getFilePath() && !this.hasDefaultImport(file, identifier)) {\n const relative = path.relative(path.dirname(file.getFilePath()), targetFile.getFilePath())\n .split(path.sep)\n .join(\"/\")\n .replace(/\\.tsx?$/, \".js\");\n file.addImportDeclaration({\n defaultImport: identifier,\n moduleSpecifier: relative.startsWith(\".\") ? relative : `./${relative}`\n });\n }\n\n return `() => ${identifier}`;\n }\n\n private hasDefaultImport(file: SourceFile, identifier: string): boolean {\n return file.getImportDeclarations().some(decl => decl.getDefaultImport()?.getText() === identifier);\n }\n\n private getDefaultExportName(file: SourceFile): string | undefined {\n const declaration = file.getDefaultExportSymbol()?.getDeclarations()[0];\n const expr = declaration?.asKind(SyntaxKind.ExportAssignment)?.getExpression();\n if (expr && expr.getKind() === SyntaxKind.Identifier) return expr.getText();\n\n for (const varDecl of file.getVariableDeclarations()) {\n if (this.unwrapCollectionObject(varDecl.getInitializer())) return varDecl.getName();\n }\n return undefined;\n }\n\n public async deleteCollection(collectionId: string) {\n const file = this.getCollectionFile(collectionId);\n if (file) {\n file.deleteImmediatelySync();\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAcA,IAAM,uCAAuB,IAAI,IAAI,CAAC,kBAAkB,CAAC;;;;;;;;;;;;;;AAezD,IAAM,sBAAsB;;;;;;;;;AAU5B,IAAM,gBAAN,MAAoB;CACY;CAA5B,YAAY,MAA8B;EAAd,KAAA,OAAA;CAC5B;AACJ;;;;;;;;;;AAWA,SAAgB,cAAc,gBAAkE;CAC5F,OAAO,wBAAwB,cAAc;AACjD;;;;;;;;;;;;;;;AAgBA,SAAgB,kBAAkB,gBAAkE;CAChG,MAAM,SAAS,cAAc,cAAc;CAC3C,MAAM,aAAa,OAAO;CAC1B,IAAI,cAAc,OAAO,eAAe,YAAY,CAAC,MAAM,QAAQ,UAAU,GACzE,OAAO,aAAa,OAAO,YACvB,OAAO,QAAQ,UAAqC,CAAC,CAAC,KAAK,CAAC,KAAK,cAAc,CAC3E,KACA,YAAY,OAAO,aAAa,YAAY,CAAC,MAAM,QAAQ,QAAQ,IAC7D,sBAAsB,QAAmC,IACzD,QACV,CAAC,CACL;CAEJ,OAAO;AACX;AAEA,IAAa,kBAAb,MAAa,gBAAgB;CACzB;CACA;CAEA,YAAY,gBAAwB;EAChC,KAAK,UAAU,IAAI,QAAQ,EACvB,sBAAsB,EAClB,iBAAiB,gBAAgB,WACrC,EACJ,CAAC;EACD,IAAI,KAAG,WAAW,cAAc,GAC5B,KAAK,QAAQ,sBAAsB,GAAG,eAAe,SAAS;EAElE,KAAK,iBAAiB,OAAK,QAAQ,cAAc;CACrD;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BA,OAAe,kBAAkB,QAAwB;EACrD,MAAM,QAAQ,OAAO,QAAQ,wBAAwB,GAAG,SAAiB,KAAK,YAAY,CAAC;EAC3F,MAAM,aAAa,6BAA6B,KAAK,KAAK,IAAI,QAAQ,IAAI,MAAM,QAAQ,WAAW,MAAM,CAAC;EAC1G,OAAO,6BAA6B,KAAK,UAAU,IAAI,aAAa,IAAI,WAAW,QAAQ,mBAAmB,EAAE;CACpH;CAEA,qBAA6B,cAA8B;EACvD,MAAM,YAAY,aAAa,QAAQ,mBAAmB,EAAE;EAC5D,IAAI,CAAC,aAAa,cAAc,cAC5B,MAAM,IAAI,MAAM,2BAA2B,aAAa,uEAAuE;EAEnI,OAAO;CACX;;;;CAKA,SAAiB,UAA0B;EACvC,MAAM,WAAW,OAAK,QAAQ,KAAK,gBAAgB,QAAQ;EAC3D,IAAI,CAAC,SAAS,WAAW,KAAK,iBAAiB,OAAK,GAAG,KAAK,aAAa,KAAK,gBAC1E,MAAM,IAAI,MAAM,8EAA8E;EAElG,OAAO;CACX;CAEA,kBAA0B,cAAsB;EAC5C,MAAM,SAAS,KAAK,qBAAqB,YAAY;EACrD,MAAM,WAAW,KAAK,SAAS,GAAG,OAAO,IAAI;EAC7C,IAAI,OAAO,KAAK,QAAQ,cAAc,QAAQ;EAC9C,IAAI,CAAC,QAAQ,KAAG,WAAW,QAAQ,GAAG;GAClC,KAAK,QAAQ,sBAAsB,GAAG,KAAK,eAAe,SAAS;GACnE,OAAO,KAAK,QAAQ,cAAc,QAAQ;EAC9C;EACA,OAAO;CACX;;;;;;;;;;;CAYA,uBAA+B,MAAwD;EACnF,IAAI,CAAC,MAAM,OAAO;EAClB,IAAI,KAAK,0BAA0B,IAAI,GAAG,OAAO;EACjD,IAAI,KAAK,0BAA0B,IAAI,KACnC,KAAK,eAAe,IAAI,KACxB,KAAK,sBAAsB,IAAI,KAC/B,KAAK,gBAAgB,IAAI,KACzB,KAAK,oBAAoB,IAAI,GAC7B,OAAO,KAAK,uBAAuB,KAAK,cAAc,CAAC;EAE3D,IAAI,KAAK,iBAAiB,IAAI,GAAG;GAE7B,MAAM,SAAS,KAAK,cAAc,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI;GAC7D,IAAI,UAAU,qBAAqB,IAAI,MAAM,GACzC,OAAO,KAAK,uBAAuB,KAAK,aAAa,CAAC,CAAC,EAAE;EAEjE;EACA,OAAO;CACX;CAEA,oBAA4B,cAAsD;EAC9E,MAAM,OAAO,KAAK,kBAAkB,YAAY;EAChD,IAAI,CAAC,MAAM,OAAO;EAElB,MAAM,gBAAgB,KAAK,uBAAuB;EAClD,IAAI,eAAe;GACf,MAAM,cAAc,cAAc,gBAAgB,CAAC,CAAC;GACpD,IAAI,eAAe,YAAY,QAAQ,MAAM,WAAW,kBAAkB;IACtE,MAAM,OAAO,YAAY,OAAO,WAAW,gBAAgB,CAAC,EAAE,cAAc;IAC5E,IAAI,QAAQ,KAAK,QAAQ,MAAM,WAAW,YAAY;KAClD,MAAM,UAAU,KAAK,QAAQ;KAC7B,MAAM,UAAU,KAAK,uBAAuB,OAAO;KACnD,MAAM,YAAY,KAAK,uBAAuB,SAAS,eAAe,CAAC;KACvE,IAAI,WAAW,OAAO;IAC1B,OAAO;KAEH,MAAM,YAAY,KAAK,uBAAuB,IAAI;KAClD,IAAI,WAAW,OAAO;IAC1B;GACJ;EACJ;EAEA,KAAK,MAAM,WAAW,KAAK,wBAAwB,GAAG;GAClD,MAAM,OAAO,KAAK,uBAAuB,QAAQ,eAAe,CAAC;GACjE,IAAI,MAAM,OAAO;EACrB;EACA,OAAO;CACX;;;;;;;;CASA,wBAAgC,cAA+C;EAC3E,MAAM,OAAO,KAAK,kBAAkB,YAAY;EAChD,IAAI,CAAC,MACD,MAAM,IAAI,MAAM,eAAe,aAAa,mBAAmB,OAAK,KAAK,KAAK,gBAAgB,GAAG,aAAa,IAAI,EAAE,EAAE;EAE1H,MAAM,gBAAgB,KAAK,oBAAoB,YAAY;EAC3D,IAAI,CAAC,eACD,MAAM,IAAI,MAAM,KAAK,sBAAsB,cAAc,IAAI,CAAC;EAElE,OAAO;CACX;CAEA,sBAA8B,cAAsB,MAA0B;EAC1E,OAAO,2CAA2C,KAAK,YAAY,EAAE,sLAGxD,aAAa;CAC9B;;CAGA,aAAqB,KAA8B,MAAoD;EACnG,OAAO,IAAI,aAAa,MACpB,aAAa,KACb,OAAQ,EAAyB,YAAY,eAC3C,EAAyB,QAAQ,MAAM,QACpC,EAAyB,QAAQ,MAAM,IAAI,KAAK,MAChD,EAAyB,QAAQ,MAAM,IAAI,KAAK,GAAG;CAChE;CAEA,OAAe,SAAS,KAAqB;EACzC,OAAO,6BAA6B,KAAK,GAAG,IAAI,MAAM,KAAK,UAAU,GAAG;CAC5E;CAEA,OAAe,cAAc,OAAkD;EAC3E,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK,KAAK,EAAE,iBAAiB;CACtG;CAEA,uBAA+B,KAAc,cAAc,GAAG,YAA8C;EAIxG,MAAM,YAAY;EAClB,MAAM,SAAS,UAAU,OAAO,WAAW;EAC3C,MAAM,cAAc,UAAU,OAAO,cAAc,CAAC;EAEpD,IAAI,eAAe,eACf,OAAO,IAAI;EAEf,IAAI,QAAQ,QAAQ,QAAQ,KAAA,GACxB,OAAO;EAEX,IAAI,OAAO,QAAQ,UACf,OAAO,KAAK,UAAU,GAAG;EAE7B,IAAI,OAAO,QAAQ,YAAY,OAAO,QAAQ,WAC1C,OAAO,OAAO,GAAG;EAErB,IAAI,MAAM,QAAQ,GAAG,GAAG;GACpB,IAAI,IAAI,WAAW,GAAG,OAAO;GAE7B,OAAO,MAAM,cADC,IAAI,KAAI,SAAQ,KAAK,uBAAuB,MAAM,cAAc,CAAC,CACpD,CAAA,CAAM,KAAK,MAAM,aAAa,EAAE,IAAI,OAAO;EAC1E;EACA,IAAI,OAAO,QAAQ,UAAU;GACzB,MAAM,SAAS;GACf,MAAM,OAAO,OAAO,KAAK,MAAM;GAG/B,MAAM,iBAA2B,CAAC;GAClC,IAAI,YAAY;IACZ,MAAM,WAAW,WAAW,cAAc;IAC1C,KAAK,MAAM,WAAW,UAClB,IAAI,QAAQ,OAAO,WAAW,kBAAkB,GAAG;KAE/C,IAAI,OADa,QAAQ,YACd,CAAA,CAAS,QAAQ;KAC5B,IAAI,KAAK,WAAW,IAAG,KAAK,KAAK,SAAS,IAAG,GAAG,OAAO,KAAK,MAAM,GAAG,EAAE;KACvE,IAAI,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,GAAG,GAAG,OAAO,KAAK,MAAM,GAAG,EAAE;KAGvE,IAAI,EAAE,QAAQ,SAAS;MACnB,MAAM,OAAO,QAAQ,eAAe;MACpC,IAAI,MAAM;OACN,MAAM,OAAO,KAAK,QAAQ;OAO1B,IANe,SAAS,WAAW,iBAC/B,SAAS,WAAW,sBACpB,SAAS,WAAW,cACpB,SAAS,WAAW,kBACpB,SAAS,WAAW,cAEV,SAAS,YAAY,SAAS,eAAe,SAAS,sBAAsB,SAAS,iBAAiB,SAAS,iBAEzH,eAAe,KAAK,GAAG,gBAAgB,SAAS,IAAI,EAAE,IAAI,KAAK,QAAQ,GAAG;MAElF;KACJ;IACJ;GAER;GAEA,IAAI,KAAK,WAAW,KAAK,eAAe,WAAW,GAAG,OAAO;GAkB7D,OAAO,MAAM,cAAc,CADT,GAfJ,KAAK,KAAI,QAAO;IAC1B,MAAM,SAAS,gBAAgB,SAAS,GAAG;IAG3C,IAAI;IACJ,IAAI,cAAc,gBAAgB,cAAc,OAAO,IAAI,GAAG;KAC1D,MAAM,UAAU,KAAK,aAAa,YAAY,GAAG;KACjD,IAAI,WAAW,QAAQ,OAAO,WAAW,kBAAkB,GACvD,eAAe,QAAQ,qBAAqB,WAAW,uBAAuB;IAEtF;IAEA,OAAO,GAAG,OAAO,IAAI,KAAK,uBAAuB,OAAO,MAAM,cAAc,GAAG,YAAY;GAC/F,CAEqB,GAAO,GAAG,cACJ,CAAA,CAAS,KAAK,MAAM,aAAa,EAAE,IAAI,OAAO;EAC7E;EACA,OAAO;CACX;;;;;;;;;CAUA,uBAA+B,QAAiC,MAA+B,aAA2B;EACtH,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,GAAG;GAC7C,MAAM,WAAW,KAAK,aAAa,QAAQ,GAAG;GAC9C,MAAM,cAAc,YAAY,SAAS,OAAO,WAAW,kBAAkB,IACvE,SAAS,qBAAqB,WAAW,uBAAuB,IAChE,KAAA;GAEN,IAAI,eAAe,gBAAgB,cAAc,KAAK,GAAG;IACrD,KAAK,uBAAuB,aAAa,OAAO,cAAc,CAAC;IAC/D;GACJ;GAEA,MAAM,cAAc,KAAK,uBAAuB,OAAO,aAAa,WAAW;GAC/E,IAAI,YAAY,SAAS,OAAO,WAAW,kBAAkB,GACzD,SAAS,eAAe,WAAW;QAEnC,OAAO,sBAAsB;IACzB,MAAM,gBAAgB,SAAS,GAAG;IAClC;GACJ,CAAC;EAET;CACJ;CAEA,MAAa,aAAa,cAAsB,aAAqB,gBAAyC;EAC1G,MAAM,gBAAgB,KAAK,wBAAwB,YAAY;EAM/D,MAAM,eAAe,sBAAsB,cAAc;EAEzD,IAAI,iBAAiB,cAAc,YAAY,YAAY;EAC3D,IAAI,CAAC,gBACD,iBAAiB,cAAc,sBAAsB;GACjD,MAAM;GACN,aAAa;EACjB,CAAC;EAGL,MAAM,WAAW,eAAe,qBAAqB,WAAW,uBAAuB;EACvF,IAAI,UAAU;GACV,MAAM,eAAe,KAAK,aAAa,UAAU,WAAW;GAE5D,IAAI;GACJ,IAAI,gBAAgB,aAAa,OAAO,WAAW,kBAAkB,GACjE,iBAAiB,aAAa,qBAAqB,WAAW,uBAAuB;GAGzF,MAAM,iBAAiB,KAAK,uBAAuB,cAAc,GAAG,cAAc;GAElF,IAAI;QACI,aAAa,OAAO,WAAW,kBAAkB,GACjD,aAAa,eAAe,cAAc;GAAA,OAG9C,SAAS,sBAAsB;IAC3B,MAAM,gBAAgB,SAAS,WAAW;IAC1C,aAAa;GACjB,CAAC;GAGL,MAAM,OAAO,KAAK,kBAAkB,YAAY;GAChD,IAAI,MACA,KAAK,WAAW;GAEpB,MAAM,KAAK,QAAQ,KAAK;EAC5B;CACJ;CAEA,MAAa,eAAe,cAAsB,aAAqB;EAGnE,MAAM,iBAFgB,KAAK,wBAAwB,YAE5B,CAAA,CAAc,YAAY,YAAY;EAC7D,IAAI,gBAAgB;GAChB,MAAM,WAAW,eAAe,qBAAqB,WAAW,uBAAuB;GACvF,IAAI,UAAU;IACV,MAAM,eAAe,KAAK,aAAa,UAAU,WAAW;IAC5D,IAAI,cAAc;KACd,aAAa,OAAO;KACpB,MAAM,OAAO,KAAK,kBAAkB,YAAY;KAChD,IAAI,MACA,KAAK,WAAW;KAEpB,MAAM,KAAK,QAAQ,KAAK;IAC5B;GACJ;EACJ;CACJ;;;;;;;;;;;;CAaA,MAAa,eAAe,cAAsB,gBAAyC,UAAiC,CAAC,GAAG;EAC5H,MAAM,UAAU,QAAQ,YAAY;EACpC,IAAI,OAAO,KAAK,kBAAkB,YAAY;EAC9C,MAAM,gBAAgB,OAAO,KAAK,oBAAoB,YAAY,IAAI;EAEtE,IAAI,QAAQ,CAAC,eAIT,MAAM,IAAI,MAAM,KAAK,sBAAsB,cAAc,IAAI,CAAC;EAGlE,IAAI,CAAC,QAAQ,CAAC,eAAe;GACzB,IAAI,SACA,MAAM,IAAI,MAAM,qCAAqC,aAAa,kCAAkC;GAGxG,MAAM,SAAS,KAAK,qBAAqB,YAAY;GACrD,MAAM,cAAc,KAAK,SAAS,GAAG,OAAO,IAAI;GAChD,IAAI,KAAG,WAAW,WAAW,GACzB,MAAM,IAAI,MAAM,yBAAyB,YAAY,gBAAgB,aAAa,0CAA0C;GAEhI,MAAM,UAAU,GAAG,gBAAgB,kBAAkB,MAAM,EAAE;GAC7D,OAAO,KAAK,QAAQ,iBAAiB,aAAa,iEAAiE,QAAQ,uBAAuB,KAAK,uBAAuB,kBAAkB,cAAc,CAAC,EAAE,sBAAsB,QAAQ,IAAI;EACvP,OAAO;GAGH,IAAI,CAAC;QAEG,EAAE,mBAAmB,mBAAmB,eAAe,kBAAkB,KAAA,KAAc,MAAM,QAAQ,eAAe,aAAa,KAAK,eAAe,cAAc,WAAW,GAAI;KAClL,MAAM,SAAS,cAAc,YAAY,eAAe;KACxD,IAAI,QACA,OAAO,OAAO;KAMlB,OAAO,eAAe;IAC1B;;GAUJ,iBAAiB,kBAAkB,cAAc;GAEjD,KAAK,MAAM,OAAO,OAAO,KAAK,cAAc,GAAG;IAC3C,IAAI,QAAQ,aAAa;KACrB,KAAK,eAAe,cAAc,MAAM,eAAe,eAAe,IAAI;KAC1E;IACJ;IAEA,MAAM,OAAO,cAAc,YAAY,GAAG;IAE1C,IAAI;IACJ,IAAI,QAAQ,KAAK,OAAO,WAAW,kBAAkB,GACjD,aAAa,KAAK,qBAAqB,WAAW,uBAAuB;IAG7E,IAAI,WAAW,cAAc,gBAAgB,cAAc,eAAe,IAAI,GAAG;KAC7E,KAAK,uBAAuB,YAAY,eAAe,MAAiC,CAAC;KACzF;IACJ;IAEA,MAAM,UAAU,KAAK,uBAAuB,eAAe,MAAM,GAAG,UAAU;IAC9E,IAAI,MACA,KAAK,eAAe,OAAO;SAE3B,cAAc,sBAAsB;KAQhC,MAAM,gBAAgB,SAAS,GAAG;KAClC,aAAa;IACjB,CAAC;GAET;EACJ;EACA,IAAI,MACA,KAAK,WAAW;EAEpB,MAAM,KAAK,QAAQ,KAAK;CAC5B;;;;;;;;;;;;;;;CAgBA,eAAuB,cAAsB,MAAkB,eAAwC,OAAsB;EACzH,IAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;EAE3B,MAAM,WAAW,KAAK,aAAa,eAAe,WAAW;EAK7D,MAAM,eAJW,YAAY,SAAS,OAAO,WAAW,kBAAkB,IACpE,SAAS,qBAAqB,WAAW,sBAAsB,IAC/D,KAAA,EAAA,EAEwB,YAAY,KAAK,CAAC;EAChD,MAAM,mCAAmB,IAAI,IAAoB;EAKjD,MAAM,0BAAkD,CAAC;EACzD,YAAY,SAAS,SAAS,UAAU;GACpC,MAAM,aAAa,QAAQ,OAAO,WAAW,uBAAuB;GACpE,IAAI,CAAC,YAAY;GACjB,MAAM,aAAa,KAAK,aAAa,YAAY,QAAQ;GACzD,MAAM,aAAa,cAAc,WAAW,OAAO,WAAW,kBAAkB,IAC1E,WAAW,eAAe,IAC1B,KAAA;GACN,IAAI,CAAC,YAAY;GAEjB,MAAM,WAAW,KAAK,aAAa,YAAY,cAAc;GAC7D,MAAM,WAAW,YAAY,SAAS,OAAO,WAAW,kBAAkB,IACpE,SAAS,qBAAqB,WAAW,aAAa,IACtD,KAAA;GACN,IAAI,UAAU,iBAAiB,IAAI,SAAS,gBAAgB,GAAG,WAAW,QAAQ,CAAC;QAC9E,wBAAwB,SAAS,WAAW,QAAQ;EAC7D,CAAC;EAED,MAAM,QAAQ,MAAM,KAAK,OAAO,UAAU;GACtC,IAAI,CAAC,gBAAgB,cAAc,KAAK,GAAG,OAAO;GAClD,MAAM,WAAoC,EAAE,GAAG,MAAM;GACrD,MAAM,eAAe,OAAO,SAAS,iBAAiB,WAAW,SAAS,eAAe,KAAA;GACzF,MAAM,YAAY,SAAS;GAE3B,IAAI;GACJ,IAAI,OAAO,cAAc,YAAY,UAAU,KAAK,CAAC,CAAC,SAAS,GAAG;IAC9D,MAAM,UAAU,UAAU,KAAK;IAQ/B,aAAa,oBAAoB,KAAK,OAAO,IACvC,UACA,KAAK,YAAY,MAAM,OAAO;GACxC,OAAO,IAAI,gBAAgB,iBAAiB,IAAI,YAAY,GACxD,aAAa,iBAAiB,IAAI,YAAY;QAC3C,IAAI,MAAM,WAAW,YAAY,QACpC,aAAa,wBAAwB;GAGzC,IAAI,CAAC,YACD,MAAM,IAAI,MAAM,aAAa,gBAAgB,IAAI,QAAQ,mBAAmB,aAAa,oDAAoD;GAGjJ,SAAS,SAAS,IAAI,cAAc,UAAU;GAC9C,OAAO;EACX,CAAC;EAED,MAAM,cAAc,KAAK,uBAAuB,OAAO,CAAC;EACxD,IAAI,YAAY,SAAS,OAAO,WAAW,kBAAkB,GACzD,SAAS,eAAe,WAAW;OAEnC,cAAc,sBAAsB;GAAE,MAAM;GAAa;EAAY,CAAC;CAE9E;;;;;CAMA,YAAoB,MAAkB,YAA4B;EAC9D,MAAM,aAAa,KAAK,kBAAkB,UAAU;EACpD,IAAI,CAAC,YACD,MAAM,IAAI,MAAM,8BAA8B,WAAW,uBAAuB,KAAK,eAAe,EAAE;EAG1G,MAAM,aAAa,KAAK,qBAAqB,UAAU;EACvD,IAAI,CAAC,YACD,MAAM,IAAI,MAAM,8BAA8B,WAAW,KAAK,WAAW,YAAY,EAAE,kCAAkC;EAG7H,IAAI,WAAW,YAAY,MAAM,KAAK,YAAY,KAAK,CAAC,KAAK,iBAAiB,MAAM,UAAU,GAAG;GAC7F,MAAM,WAAW,OAAK,SAAS,OAAK,QAAQ,KAAK,YAAY,CAAC,GAAG,WAAW,YAAY,CAAC,CAAC,CACrF,MAAM,OAAK,GAAG,CAAC,CACf,KAAK,GAAG,CAAC,CACT,QAAQ,WAAW,KAAK;GAC7B,KAAK,qBAAqB;IACtB,eAAe;IACf,iBAAiB,SAAS,WAAW,GAAG,IAAI,WAAW,KAAK;GAChE,CAAC;EACL;EAEA,OAAO,SAAS;CACpB;CAEA,iBAAyB,MAAkB,YAA6B;EACpE,OAAO,KAAK,sBAAsB,CAAC,CAAC,MAAK,SAAQ,KAAK,iBAAiB,CAAC,EAAE,QAAQ,MAAM,UAAU;CACtG;CAEA,qBAA6B,MAAsC;EAE/D,MAAM,QADc,KAAK,uBAAuB,CAAC,EAAE,gBAAgB,CAAC,CAAC,GAAA,EAC3C,OAAO,WAAW,gBAAgB,CAAC,EAAE,cAAc;EAC7E,IAAI,QAAQ,KAAK,QAAQ,MAAM,WAAW,YAAY,OAAO,KAAK,QAAQ;EAE1E,KAAK,MAAM,WAAW,KAAK,wBAAwB,GAC/C,IAAI,KAAK,uBAAuB,QAAQ,eAAe,CAAC,GAAG,OAAO,QAAQ,QAAQ;CAG1F;CAEA,MAAa,iBAAiB,cAAsB;EAChD,MAAM,OAAO,KAAK,kBAAkB,YAAY;EAChD,IAAI,MACA,KAAK,sBAAsB;CAEnC;AACJ"}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Permission guard for Service API Keys.
|
|
3
|
-
*
|
|
4
|
-
* Checks whether an API key's permission set allows a specific operation
|
|
5
|
-
* on a specific collection. Used by the REST API generator middleware to
|
|
6
|
-
* enforce fine-grained access control.
|
|
7
|
-
*
|
|
8
|
-
* @module
|
|
9
|
-
*/
|
|
10
|
-
import type { ApiKeyPermission } from "./api-key-types.js";
|
|
11
|
-
/** Operations that map to HTTP methods. */
|
|
12
|
-
export type ApiKeyOperation = "read" | "write" | "delete";
|
|
13
|
-
/**
|
|
14
|
-
* Map an HTTP method string to an `ApiKeyOperation`.
|
|
15
|
-
*
|
|
16
|
-
* - `GET`, `HEAD`, `OPTIONS` → `"read"`
|
|
17
|
-
* - `POST`, `PUT`, `PATCH` → `"write"`
|
|
18
|
-
* - `DELETE` → `"delete"`
|
|
19
|
-
*/
|
|
20
|
-
export declare function httpMethodToOperation(method: string): ApiKeyOperation;
|
|
21
|
-
/**
|
|
22
|
-
* Check whether the given permissions array allows `operation` on `collection`.
|
|
23
|
-
*
|
|
24
|
-
* Supports the `"*"` wildcard for the collection field, which matches any
|
|
25
|
-
* collection. Returns `true` if at least one permission entry grants access.
|
|
26
|
-
*
|
|
27
|
-
* @param permissions - The API key's permission entries.
|
|
28
|
-
* @param collection - The target collection slug.
|
|
29
|
-
* @param operation - The requested operation.
|
|
30
|
-
* @returns `true` if the operation is permitted.
|
|
31
|
-
*/
|
|
32
|
-
export declare function isOperationAllowed(permissions: ApiKeyPermission[], collection: string, operation: ApiKeyOperation): boolean;
|
|
33
|
-
/**
|
|
34
|
-
* Check whether the given permissions array allows a storage operation.
|
|
35
|
-
*
|
|
36
|
-
* Storage, like functions, lives outside the collection namespace: an entry
|
|
37
|
-
* with collection `"storage"` grants storage access for the listed
|
|
38
|
-
* operations (GET routes → `read`, upload/folder/tus → `write`,
|
|
39
|
-
* DELETE routes → `delete`), and the global `"*"` wildcard also matches.
|
|
40
|
-
* A collection-scoped key gets no storage access at all.
|
|
41
|
-
*
|
|
42
|
-
* Matching is exactly {@link isOperationAllowed} with `"storage"` as the
|
|
43
|
-
* resource name — delegated so the semantics can never drift.
|
|
44
|
-
*/
|
|
45
|
-
export declare function isStorageAllowed(permissions: ApiKeyPermission[], operation: ApiKeyOperation): boolean;
|
|
46
|
-
/**
|
|
47
|
-
* Check whether the given permissions array allows invoking a custom function.
|
|
48
|
-
*
|
|
49
|
-
* Functions live outside the collection namespace, so they have their own
|
|
50
|
-
* resource names in the permission list:
|
|
51
|
-
*
|
|
52
|
-
* - `{ collection: "functions", ... }` grants every function
|
|
53
|
-
* - `{ collection: "functions/<name>", ... }` grants a single function
|
|
54
|
-
* - the global `"*"` wildcard (full-access keys) also matches
|
|
55
|
-
*
|
|
56
|
-
* The operation is derived from the HTTP method as usual (GET → read,
|
|
57
|
-
* POST/PUT/PATCH → write, DELETE → delete). The functions index route is
|
|
58
|
-
* checked as `functionName === ""`, which only the `"functions"` and `"*"`
|
|
59
|
-
* entries can grant.
|
|
60
|
-
*
|
|
61
|
-
* A collection-scoped key (e.g. read-only on `events`) therefore can NOT
|
|
62
|
-
* invoke functions — before this guard existed, any valid key could call
|
|
63
|
-
* every function.
|
|
64
|
-
*/
|
|
65
|
-
export declare function isFunctionAllowed(permissions: ApiKeyPermission[], functionName: string, operation: ApiKeyOperation): boolean;
|