@rebasepro/server-core 0.6.1 → 0.8.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.
Files changed (116) hide show
  1. package/dist/api/graphql/graphql-schema-generator.d.ts +9 -0
  2. package/dist/api/rest/api-generator.d.ts +2 -14
  3. package/dist/api/rest/query-parser.d.ts +2 -5
  4. package/dist/api/types.d.ts +8 -2
  5. package/dist/auth/adapter-middleware.d.ts +7 -1
  6. package/dist/auth/admin-roles-route.d.ts +18 -0
  7. package/dist/auth/admin-users-route.d.ts +28 -0
  8. package/dist/auth/api-keys/api-key-types.d.ts +8 -0
  9. package/dist/auth/auth-hooks.d.ts +17 -0
  10. package/dist/auth/builtin-auth-adapter.d.ts +3 -1
  11. package/dist/auth/index.d.ts +1 -0
  12. package/dist/auth/interfaces.d.ts +32 -2
  13. package/dist/auth/magic-link-routes.d.ts +30 -0
  14. package/dist/auth/mfa-crypto.d.ts +23 -0
  15. package/dist/auth/mfa-routes.d.ts +2 -1
  16. package/dist/auth/middleware.d.ts +9 -2
  17. package/dist/auth/routes.d.ts +3 -1
  18. package/dist/auth/session-routes.d.ts +2 -0
  19. package/dist/backend-CIxN4FVm.js.map +1 -1
  20. package/dist/base64-js-C_frYBkI.js +1687 -0
  21. package/dist/base64-js-C_frYBkI.js.map +1 -0
  22. package/dist/{dist-CZKP-Xz4.js → dist-DMO-zF6D.js} +2 -2
  23. package/dist/{dist-CZKP-Xz4.js.map → dist-DMO-zF6D.js.map} +1 -1
  24. package/dist/email/index.d.ts +2 -2
  25. package/dist/email/templates.d.ts +8 -0
  26. package/dist/email/types.d.ts +18 -0
  27. package/dist/functions/define-function.d.ts +50 -0
  28. package/dist/functions/index.d.ts +2 -0
  29. package/dist/index.d.ts +16 -18
  30. package/dist/index.es.js +1947 -2356
  31. package/dist/index.es.js.map +1 -1
  32. package/dist/index.umd.js +40615 -10203
  33. package/dist/index.umd.js.map +1 -1
  34. package/dist/init.d.ts +68 -12
  35. package/dist/jws-BqRRaK11.js +616 -0
  36. package/dist/jws-BqRRaK11.js.map +1 -0
  37. package/dist/{jwt-DHcQRGC3.js → jwt-BETC8a1J.js} +3 -611
  38. package/dist/jwt-BETC8a1J.js.map +1 -0
  39. package/dist/services/routed-realtime-service.d.ts +43 -0
  40. package/dist/{src-COaj0G3P.js → src-CB2PIpBe.js} +2 -2
  41. package/dist/{src-COaj0G3P.js.map → src-CB2PIpBe.js.map} +1 -1
  42. package/dist/src-DjzOT1kG.js +29746 -0
  43. package/dist/src-DjzOT1kG.js.map +1 -0
  44. package/dist/storage/GCSStorageController.d.ts +43 -0
  45. package/dist/storage/index.d.ts +5 -2
  46. package/dist/storage/routes.d.ts +33 -1
  47. package/dist/storage/tus-handler.d.ts +3 -1
  48. package/dist/storage/types.d.ts +28 -3
  49. package/dist/utils/dev-port.d.ts +2 -0
  50. package/package.json +13 -5
  51. package/src/api/errors.ts +16 -2
  52. package/src/api/graphql/graphql-schema-generator.ts +50 -15
  53. package/src/api/openapi-generator.ts +2 -2
  54. package/src/api/rest/api-generator.ts +44 -123
  55. package/src/api/rest/query-parser.ts +6 -23
  56. package/src/api/server.ts +10 -2
  57. package/src/api/types.ts +8 -2
  58. package/src/auth/adapter-middleware.ts +10 -2
  59. package/src/auth/admin-roles-route.ts +36 -0
  60. package/src/auth/admin-users-route.ts +302 -0
  61. package/src/auth/api-keys/api-key-middleware.ts +4 -3
  62. package/src/auth/api-keys/api-key-routes.ts +12 -2
  63. package/src/auth/api-keys/api-key-store.ts +83 -66
  64. package/src/auth/api-keys/api-key-types.ts +8 -0
  65. package/src/auth/apple-oauth.ts +2 -1
  66. package/src/auth/auth-hooks.ts +21 -0
  67. package/src/auth/bitbucket-oauth.ts +2 -1
  68. package/src/auth/builtin-auth-adapter.ts +29 -6
  69. package/src/auth/custom-auth-adapter.ts +2 -0
  70. package/src/auth/discord-oauth.ts +2 -1
  71. package/src/auth/facebook-oauth.ts +2 -1
  72. package/src/auth/github-oauth.ts +2 -1
  73. package/src/auth/gitlab-oauth.ts +2 -1
  74. package/src/auth/google-oauth.ts +8 -4
  75. package/src/auth/index.ts +2 -0
  76. package/src/auth/interfaces.ts +38 -2
  77. package/src/auth/linkedin-oauth.ts +2 -1
  78. package/src/auth/magic-link-routes.ts +167 -0
  79. package/src/auth/mfa-crypto.ts +91 -0
  80. package/src/auth/mfa-routes.ts +34 -10
  81. package/src/auth/microsoft-oauth.ts +2 -1
  82. package/src/auth/middleware.ts +14 -5
  83. package/src/auth/reset-password-admin.ts +17 -1
  84. package/src/auth/routes.ts +78 -10
  85. package/src/auth/session-routes.ts +15 -3
  86. package/src/auth/slack-oauth.ts +2 -1
  87. package/src/auth/spotify-oauth.ts +2 -1
  88. package/src/auth/twitter-oauth.ts +8 -1
  89. package/src/cron/cron-store.ts +25 -23
  90. package/src/email/index.ts +3 -2
  91. package/src/email/templates.ts +82 -0
  92. package/src/email/types.ts +16 -0
  93. package/src/functions/define-function.ts +59 -0
  94. package/src/functions/function-loader.ts +16 -0
  95. package/src/functions/index.ts +2 -0
  96. package/src/index.ts +70 -37
  97. package/src/init.ts +214 -25
  98. package/src/services/routed-realtime-service.ts +113 -0
  99. package/src/storage/GCSStorageController.ts +334 -0
  100. package/src/storage/index.ts +9 -3
  101. package/src/storage/routes.ts +191 -23
  102. package/src/storage/tus-handler.ts +16 -4
  103. package/src/storage/types.ts +25 -3
  104. package/src/utils/dev-port.ts +13 -7
  105. package/test/api-generator.test.ts +1 -1
  106. package/test/auth-config-types.test.ts +40 -0
  107. package/test/auth-routes.test.ts +54 -4
  108. package/test/custom-auth-adapter.test.ts +20 -1
  109. package/test/define-function.test.ts +45 -0
  110. package/test/env.test.ts +9 -19
  111. package/test/multi-datasource-routing.test.ts +113 -0
  112. package/test/routed-realtime-service.test.ts +86 -0
  113. package/test/storage-routes.test.ts +160 -0
  114. package/test/transform-auth-response.test.ts +305 -0
  115. package/dist/jwt-DHcQRGC3.js.map +0 -1
  116. package/test/backend-hooks-data.test.ts +0 -477
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Google Cloud Storage controller (works with GCS and Firebase Storage)
3
+ *
4
+ * The `@google-cloud/storage` package is loaded lazily so it remains an
5
+ * optional peer dependency — users who don't need GCS never pay for the
6
+ * import cost or need it installed.
7
+ */
8
+ import { StorageController, GCSStorageConfig } from "./types";
9
+ import { UploadFileProps, UploadFileResult, DownloadConfig, StorageListResult } from "@rebasepro/types";
10
+ /**
11
+ * Google Cloud Storage implementation of `StorageController`.
12
+ *
13
+ * Works with standard GCS buckets **and** Firebase Storage buckets
14
+ * (e.g. `"my-project.appspot.com"`).
15
+ */
16
+ export declare class GCSStorageController implements StorageController {
17
+ private config;
18
+ private client;
19
+ constructor(config: GCSStorageConfig);
20
+ private getClient;
21
+ getType(): "gcs";
22
+ putObject({ file, key, metadata, bucket }: UploadFileProps): Promise<UploadFileResult>;
23
+ getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig>;
24
+ getObject(key: string, bucket?: string): Promise<File | null>;
25
+ deleteObject(key: string, bucket?: string): Promise<void>;
26
+ listObjects(prefix: string, options?: {
27
+ bucket?: string;
28
+ maxResults?: number;
29
+ pageToken?: string;
30
+ }): Promise<StorageListResult>;
31
+ /**
32
+ * Validate file before upload
33
+ */
34
+ private validateFile;
35
+ /**
36
+ * Get the bucket name — either from parameter or config.
37
+ */
38
+ private getBucket;
39
+ /**
40
+ * Flatten nested metadata to string values (GCS custom metadata requirement)
41
+ */
42
+ private flattenMetadata;
43
+ }
@@ -1,17 +1,20 @@
1
1
  /**
2
2
  * Storage module for Rebase backend
3
3
  *
4
- * Provides pluggable file storage with two built-in providers:
4
+ * Provides pluggable file storage with three built-in providers:
5
5
  * - **Local filesystem** — zero config, great for dev and single-server deployments.
6
6
  * - **S3-compatible** — works with AWS S3, Cloudflare R2, MinIO, Hetzner Object Storage,
7
7
  * Backblaze B2, DigitalOcean Spaces, and GCS (via S3 interop).
8
+ * - **Google Cloud Storage / Firebase Storage** — native GCS support via `@google-cloud/storage`
9
+ * (optional peer dependency, lazily loaded).
8
10
  *
9
- * For other providers (native GCS SDK, Azure Blob, etc.), implement the
11
+ * For other providers (Azure Blob, etc.), implement the
10
12
  * `StorageController` interface and pass the instance directly to the `storage` config.
11
13
  */
12
14
  export * from "./types";
13
15
  export { LocalStorageController } from "./LocalStorageController";
14
16
  export { S3StorageController } from "./S3StorageController";
17
+ export { GCSStorageController } from "./GCSStorageController";
15
18
  export { createStorageRoutes } from "./routes";
16
19
  export type { StorageRoutesConfig } from "./routes";
17
20
  export * from "./storage-registry";
@@ -1,11 +1,36 @@
1
1
  /**
2
2
  * Storage REST API routes using Hono
3
+ *
4
+ * Supports multi-backend routing via `StorageRegistry`. Each endpoint
5
+ * accepts an optional `storageId` parameter (query string or form field)
6
+ * to target a named storage backend. When omitted, the default backend
7
+ * is used.
3
8
  */
4
9
  import { Hono } from "hono";
5
10
  import { StorageController } from "./types";
11
+ import type { StorageRegistry } from "./storage-registry";
12
+ import { type StorageSourceDefinition, type AuthAdapter } from "@rebasepro/types";
6
13
  import { HonoEnv } from "../api/types";
7
14
  export interface StorageRoutesConfig {
8
- controller: StorageController;
15
+ /**
16
+ * Single storage controller (backward-compatible).
17
+ * Used as fallback when no `registry` is provided.
18
+ */
19
+ controller?: StorageController;
20
+ /**
21
+ * Full storage registry for multi-backend routing.
22
+ * When provided, endpoints resolve the controller from `storageId`
23
+ * parameter. Takes precedence over `controller`.
24
+ */
25
+ registry?: StorageRegistry;
26
+ /**
27
+ * Declared storage sources, surfaced by `GET /sources` so the client can
28
+ * bootstrap its registry. Carries the frontend `transport` (server vs
29
+ * direct) and human-readable labels. Server-transport sources are also
30
+ * derived from the registry; `direct` sources (e.g. Firebase Storage) only
31
+ * exist here since the backend does not proxy them.
32
+ */
33
+ sources?: StorageSourceDefinition[];
9
34
  /** Base path for storage routes (default: '/api/storage') */
10
35
  basePath?: string;
11
36
  /** Require authentication for write operations (default: true) */
@@ -13,6 +38,13 @@ export interface StorageRoutesConfig {
13
38
  /** Allow unauthenticated read access to stored files (default: false).
14
39
  * When false and requireAuth is true, reads also require authentication. */
15
40
  publicRead?: boolean;
41
+ /**
42
+ * When provided, storage routes delegate auth to this adapter instead
43
+ * of the built-in JWT module. This mirrors how data routes use
44
+ * `createAdapterAuthMiddleware()` and avoids the "JWT secret not
45
+ * configured" crash when `configureJwt()` was never called.
46
+ */
47
+ authAdapter?: AuthAdapter;
16
48
  }
17
49
  /**
18
50
  * Extract the wildcard portion of a route path from the full request path.
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import type { Context } from "hono";
11
11
  import type { StorageController } from "./types";
12
+ import type { StorageRegistry } from "./storage-registry";
12
13
  /**
13
14
  * TUS resumable upload handler.
14
15
  *
@@ -18,10 +19,11 @@ import type { StorageController } from "./types";
18
19
  */
19
20
  export declare class TusHandler {
20
21
  private storageController?;
22
+ private storageRegistry?;
21
23
  private uploads;
22
24
  private tusDir;
23
25
  private cleanupTimer?;
24
- constructor(storageBaseDir: string, storageController?: StorageController | undefined);
26
+ constructor(storageBaseDir: string, storageController?: StorageController | undefined, storageRegistry?: StorageRegistry | undefined);
25
27
  /** Ensure the temp directory exists. */
26
28
  private ensureDir;
27
29
  /** Start periodic cleanup of stale uploads. */
@@ -41,18 +41,43 @@ export interface S3StorageConfig {
41
41
  signedUrlExpiration?: number;
42
42
  }
43
43
  /**
44
- * Storage configuration local filesystem or S3-compatible.
44
+ * Google Cloud Storage configuration (works with GCS and Firebase Storage)
45
+ */
46
+ export interface GCSStorageConfig {
47
+ type: "gcs";
48
+ /** GCS bucket name (e.g. "my-project.appspot.com" for Firebase Storage) */
49
+ bucket: string;
50
+ /** GCP project ID (optional, auto-detected from credentials) */
51
+ projectId?: string;
52
+ /** Path to service account JSON key file */
53
+ keyFilename?: string;
54
+ /** Service account credentials object (alternative to keyFilename) */
55
+ credentials?: {
56
+ client_email: string;
57
+ private_key: string;
58
+ project_id?: string;
59
+ };
60
+ /** Maximum file size in bytes (default: 50MB) */
61
+ maxFileSize?: number;
62
+ /** Allowed MIME types */
63
+ allowedMimeTypes?: string[];
64
+ /** Signed URL expiration in seconds (default: 3600) */
65
+ signedUrlExpiration?: number;
66
+ }
67
+ /**
68
+ * Storage configuration — local filesystem, S3-compatible, or Google Cloud Storage.
45
69
  *
46
70
  * **Built-in providers:**
47
71
  * - `local` — Zero-config filesystem storage. Great for dev and single-server deployments (Hetzner, bare metal).
48
72
  * - `s3` — Any S3-compatible provider. AWS S3, Cloudflare R2, MinIO, Hetzner Object Storage,
49
73
  * Backblaze B2, DigitalOcean Spaces, and even GCS (via its S3-compatible interoperability API).
74
+ * - `gcs` — Native Google Cloud Storage / Firebase Storage via `@google-cloud/storage`.
50
75
  *
51
76
  * **Custom providers:**
52
- * For cloud-native storage (GCS, Azure Blob, etc.), implement the `StorageController`
77
+ * For other cloud storage (Azure Blob, etc.), implement the `StorageController`
53
78
  * interface and pass the instance directly to the `storage` config.
54
79
  */
55
- export type BackendStorageConfig = LocalStorageConfig | S3StorageConfig;
80
+ export type BackendStorageConfig = LocalStorageConfig | S3StorageConfig | GCSStorageConfig;
56
81
  /**
57
82
  * Storage controller interface for backend implementations
58
83
  */
@@ -28,6 +28,8 @@ export declare function listenWithPortRetry(server: Server, startPort: number, o
28
28
  maxAttempts?: number;
29
29
  /** Absolute path to write the resolved port file into. Defaults to `process.cwd()`. */
30
30
  portFileDir?: string;
31
+ /** Service key to include in the state file for MCP server auto-discovery. */
32
+ serviceKey?: string;
31
33
  }): Promise<number>;
32
34
  /**
33
35
  * Clean up the dev port file and state file (call on graceful shutdown).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/server-core",
3
3
  "type": "module",
4
- "version": "0.6.1",
4
+ "version": "0.8.0",
5
5
  "description": "Database-Agnostic Backend Core for Rebase",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -54,10 +54,10 @@
54
54
  "ts-morph": "28.0.0",
55
55
  "ws": "^8.21.0",
56
56
  "zod": "^4.4.3",
57
- "@rebasepro/client": "0.6.1",
58
- "@rebasepro/types": "0.6.1",
59
- "@rebasepro/utils": "0.6.1",
60
- "@rebasepro/common": "0.6.1"
57
+ "@rebasepro/client": "0.8.0",
58
+ "@rebasepro/common": "0.8.0",
59
+ "@rebasepro/types": "0.8.0",
60
+ "@rebasepro/utils": "0.8.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/jest": "^30.0.0",
@@ -74,6 +74,14 @@
74
74
  "typescript": "^6.0.3",
75
75
  "vite": "^8.0.16"
76
76
  },
77
+ "peerDependencies": {
78
+ "@google-cloud/storage": "^7.0.0"
79
+ },
80
+ "peerDependenciesMeta": {
81
+ "@google-cloud/storage": {
82
+ "optional": true
83
+ }
84
+ },
77
85
  "gitHead": "d935eefa5aa8d1009a2398cfac2c1e4ee9aeb6b6",
78
86
  "publishConfig": {
79
87
  "access": "public"
package/src/api/errors.ts CHANGED
@@ -145,12 +145,26 @@ export const errorHandler: ErrorHandler<HonoEnv> = (err, c) => {
145
145
 
146
146
  // Handle DB connection and specific system errors for better logging
147
147
  let logMessage = error.message;
148
+
149
+ // Resolve the actual cause — Node's net module wraps dual-stack failures
150
+ // in an AggregateError whose inner errors carry the real address/port.
151
+ let resolvedCause: PgLikeError | undefined;
148
152
  if (error.cause && typeof error.cause === "object" && error.cause !== null && "code" in error.cause) {
149
- const cause = error.cause as PgLikeError;
153
+ const cause = error.cause as PgLikeError & { errors?: PgLikeError[] };
154
+ if (cause.code === "ECONNREFUSED" && !cause.address && Array.isArray(cause.errors)) {
155
+ // AggregateError — pick the first inner error that has address info
156
+ resolvedCause = cause.errors.find(e => e.address) || cause;
157
+ } else {
158
+ resolvedCause = cause;
159
+ }
160
+ }
161
+
162
+ if (resolvedCause) {
163
+ const cause = resolvedCause;
150
164
  if (cause.code === "ENETUNREACH") {
151
165
  logMessage = `Network unreachable. Cannot connect to database at ${cause.address}:${cause.port}.`;
152
166
  } else if (cause.code === "ECONNREFUSED") {
153
- logMessage = `Connection refused to database at ${cause.address}:${cause.port}.`;
167
+ logMessage = `Connection refused to database at ${cause.address}:${cause.port}. Is PostgreSQL running?`;
154
168
  } else if (cause.code === "42703" || cause.code === "42P01") {
155
169
  code = "SCHEMA_DRIFT";
156
170
  const issue = cause.code === "42703" ? "column" : "table";
@@ -11,7 +11,15 @@ import {
11
11
  GraphQLInputObjectType,
12
12
  GraphQLInputFieldConfig
13
13
  } from "graphql";
14
- import { DataDriver, EntityCollection, FetchCollectionProps, Property, Entity } from "@rebasepro/types";
14
+ import { DataDriver, EntityCollection, FetchCollectionProps, Property, Entity, getCollectionDataPath } from "@rebasepro/types";
15
+ import { isOperationAllowed, type ApiKeyOperation } from "../../auth/api-keys/api-key-permission-guard";
16
+ import type { ApiKeyMasked } from "../../auth/api-keys/api-key-types";
17
+
18
+ /** Context shape provided by @hono/graphql-server (Hono's Context object). */
19
+ type GraphQLResolverContext = {
20
+ get?: (key: string) => unknown;
21
+ driver?: DataDriver;
22
+ };
15
23
 
16
24
  /**
17
25
  * Lightweight GraphQL schema generator that leverages existing DataDriver
@@ -28,6 +36,28 @@ export class GraphQLSchemaGenerator {
28
36
  this.driver = driver;
29
37
  }
30
38
 
39
+ /**
40
+ * Enforce API key permission scoping for a GraphQL resolver.
41
+ *
42
+ * Extracts the `apiKey` from the Hono context passed by `@hono/graphql-server`.
43
+ * If the request was made with an API key and the key does not have the
44
+ * required permission for the target collection/operation, an error is thrown.
45
+ * Non-API-key requests (e.g. session auth) are allowed through.
46
+ */
47
+ private enforceApiKeyPermission(
48
+ context: GraphQLResolverContext | undefined,
49
+ collectionSlug: string,
50
+ operation: ApiKeyOperation
51
+ ): void {
52
+ const apiKey = context?.get?.("apiKey") as ApiKeyMasked | undefined;
53
+ if (!apiKey) return;
54
+ if (!isOperationAllowed(apiKey.permissions, collectionSlug, operation)) {
55
+ throw new Error(
56
+ `API key does not have "${operation}" permission for collection "${collectionSlug}"`
57
+ );
58
+ }
59
+ }
60
+
31
61
  /**
32
62
  * Generate complete GraphQL schema using existing DataDriver
33
63
  */
@@ -184,11 +214,12 @@ export class GraphQLSchemaGenerator {
184
214
  id: { type: new GraphQLNonNull(GraphQLString) }
185
215
  },
186
216
  resolve: async (_, args, context: unknown) => {
187
- const ctx = context as { driver?: DataDriver } | undefined;
188
- const ds = ctx?.driver;
217
+ const ctx = context as GraphQLResolverContext | undefined;
218
+ this.enforceApiKeyPermission(ctx, collection.slug, "read");
219
+ const ds = ctx?.get?.("driver") as DataDriver | undefined ?? (ctx as Record<string, unknown> | undefined)?.driver as DataDriver | undefined;
189
220
  if (!ds) throw new Error("Scoped driver not available");
190
221
  const entity = await ds.fetchEntity({
191
- path: collection.slug,
222
+ path: getCollectionDataPath(collection),
192
223
  entityId: args.id,
193
224
  collection
194
225
  });
@@ -208,8 +239,9 @@ defaultValue: 0 },
208
239
  orderBy: { type: GraphQLString }
209
240
  },
210
241
  resolve: async (_, args, context: unknown) => {
211
- const ctx = context as { driver?: DataDriver } | undefined;
212
- const ds = ctx?.driver;
242
+ const ctx = context as GraphQLResolverContext | undefined;
243
+ this.enforceApiKeyPermission(ctx, collection.slug, "read");
244
+ const ds = ctx?.get?.("driver") as DataDriver | undefined ?? (ctx as Record<string, unknown> | undefined)?.driver as DataDriver | undefined;
213
245
  if (!ds) throw new Error("Scoped driver not available");
214
246
  let filter: FetchCollectionProps["filter"] | undefined;
215
247
  if (args.where) {
@@ -224,7 +256,7 @@ defaultValue: 0 },
224
256
  }
225
257
  }
226
258
  const entities = await ds.fetchCollection({
227
- path: collection.slug,
259
+ path: getCollectionDataPath(collection),
228
260
  collection,
229
261
  filter,
230
262
  limit: args.limit,
@@ -261,8 +293,9 @@ defaultValue: 0 },
261
293
  input: { type: new GraphQLNonNull(inputType) }
262
294
  },
263
295
  resolve: async (_, args, context: unknown) => {
264
- const ctx = context as { driver?: DataDriver } | undefined;
265
- const ds = ctx?.driver;
296
+ const ctx = context as GraphQLResolverContext | undefined;
297
+ this.enforceApiKeyPermission(ctx, collection.slug, "write");
298
+ const ds = ctx?.get?.("driver") as DataDriver | undefined ?? (ctx as Record<string, unknown> | undefined)?.driver as DataDriver | undefined;
266
299
  if (!ds) throw new Error("Scoped driver not available");
267
300
  const path = collection.slug;
268
301
  const entity = await ds.saveEntity({
@@ -283,11 +316,12 @@ defaultValue: 0 },
283
316
  input: { type: new GraphQLNonNull(inputType) }
284
317
  },
285
318
  resolve: async (_, args, context: unknown) => {
286
- const ctx = context as { driver?: DataDriver } | undefined;
287
- const ds = ctx?.driver;
319
+ const ctx = context as GraphQLResolverContext | undefined;
320
+ this.enforceApiKeyPermission(ctx, collection.slug, "write");
321
+ const ds = ctx?.get?.("driver") as DataDriver | undefined ?? (ctx as Record<string, unknown> | undefined)?.driver as DataDriver | undefined;
288
322
  if (!ds) throw new Error("Scoped driver not available");
289
323
  const entity = await ds.saveEntity({
290
- path: collection.slug,
324
+ path: getCollectionDataPath(collection),
291
325
  entityId: args.id,
292
326
  values: args.input,
293
327
  collection,
@@ -304,12 +338,13 @@ defaultValue: 0 },
304
338
  id: { type: new GraphQLNonNull(GraphQLString) }
305
339
  },
306
340
  resolve: async (_, args, context: unknown) => {
341
+ const ctx = context as GraphQLResolverContext | undefined;
342
+ this.enforceApiKeyPermission(ctx, collection.slug, "delete");
307
343
  try {
308
- const ctx = context as { driver?: DataDriver } | undefined;
309
- const ds = ctx?.driver;
344
+ const ds = ctx?.get?.("driver") as DataDriver | undefined ?? (ctx as Record<string, unknown> | undefined)?.driver as DataDriver | undefined;
310
345
  if (!ds) throw new Error("Scoped driver not available");
311
346
  const existingEntity = await ds.fetchEntity({
312
- path: collection.slug,
347
+ path: getCollectionDataPath(collection),
313
348
  entityId: args.id,
314
349
  collection
315
350
  });
@@ -491,9 +491,9 @@ function convertPropertyToSchema(property: Property): Record<string, unknown> {
491
491
  }
492
492
 
493
493
  if (sp.email) base.format = "email";
494
- if (sp.url) base.format = "uri";
494
+ if (sp.ui?.url) base.format = "uri";
495
495
  if (sp.storage) base.format = "uri";
496
- if (sp.markdown) base.description = (base.description || "") + " (Markdown)";
496
+ if (sp.ui?.markdown) base.description = (base.description || "") + " (Markdown)";
497
497
 
498
498
  return base;
499
499
  }