@rebasepro/server-core 0.7.0 → 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 (155) hide show
  1. package/build-errors.txt +52 -0
  2. package/dist/api/ast-schema-editor.d.ts +21 -0
  3. package/dist/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
  4. package/dist/api/errors.d.ts +53 -0
  5. package/dist/api/graphql/graphql-schema-generator.d.ts +44 -0
  6. package/dist/api/graphql/index.d.ts +1 -0
  7. package/dist/api/index.d.ts +9 -0
  8. package/dist/api/logs-routes.d.ts +37 -0
  9. package/dist/api/openapi-generator.d.ts +16 -0
  10. package/dist/api/rest/api-generator.d.ts +76 -0
  11. package/dist/api/rest/index.d.ts +1 -0
  12. package/dist/api/rest/query-parser.d.ts +8 -0
  13. package/dist/api/schema-editor-routes.d.ts +3 -0
  14. package/dist/api/server.d.ts +40 -0
  15. package/dist/api/types.d.ts +104 -0
  16. package/dist/auth/adapter-middleware.d.ts +43 -0
  17. package/dist/auth/admin-roles-route.d.ts +18 -0
  18. package/dist/auth/admin-user-ops.d.ts +79 -0
  19. package/dist/auth/admin-users-route.d.ts +28 -0
  20. package/dist/auth/api-keys/api-key-middleware.d.ts +39 -0
  21. package/dist/auth/api-keys/api-key-permission-guard.d.ts +32 -0
  22. package/dist/auth/api-keys/api-key-routes.d.ts +20 -0
  23. package/dist/auth/api-keys/api-key-store.d.ts +35 -0
  24. package/dist/auth/api-keys/api-key-types.d.ts +96 -0
  25. package/dist/auth/api-keys/index.d.ts +17 -0
  26. package/dist/auth/apple-oauth.d.ts +30 -0
  27. package/dist/auth/auth-hooks.d.ts +242 -0
  28. package/dist/auth/bitbucket-oauth.d.ts +11 -0
  29. package/dist/auth/builtin-auth-adapter.d.ts +51 -0
  30. package/dist/auth/crypto-utils.d.ts +16 -0
  31. package/dist/auth/custom-auth-adapter.d.ts +39 -0
  32. package/dist/auth/discord-oauth.d.ts +14 -0
  33. package/dist/auth/facebook-oauth.d.ts +14 -0
  34. package/dist/auth/github-oauth.d.ts +15 -0
  35. package/dist/auth/gitlab-oauth.d.ts +13 -0
  36. package/dist/auth/google-oauth.d.ts +47 -0
  37. package/dist/auth/index.d.ts +37 -0
  38. package/dist/auth/interfaces.d.ts +431 -0
  39. package/dist/auth/jwt.d.ts +55 -0
  40. package/dist/auth/linkedin-oauth.d.ts +18 -0
  41. package/dist/auth/magic-link-routes.d.ts +30 -0
  42. package/dist/auth/mfa-crypto.d.ts +23 -0
  43. package/dist/auth/mfa-routes.d.ts +7 -0
  44. package/dist/auth/mfa.d.ts +49 -0
  45. package/dist/auth/microsoft-oauth.d.ts +16 -0
  46. package/dist/auth/middleware.d.ts +113 -0
  47. package/dist/auth/password.d.ts +22 -0
  48. package/dist/auth/rate-limiter.d.ts +50 -0
  49. package/dist/auth/reset-password-admin.d.ts +29 -0
  50. package/dist/auth/rls-scope.d.ts +31 -0
  51. package/dist/auth/routes.d.ts +35 -0
  52. package/dist/auth/session-routes.d.ts +27 -0
  53. package/dist/auth/slack-oauth.d.ts +12 -0
  54. package/dist/auth/spotify-oauth.d.ts +12 -0
  55. package/dist/auth/twitter-oauth.d.ts +18 -0
  56. package/dist/backend-CIxN4FVm.js +15 -0
  57. package/dist/backend-CIxN4FVm.js.map +1 -0
  58. package/dist/base64-js-C_frYBkI.js +1687 -0
  59. package/dist/base64-js-C_frYBkI.js.map +1 -0
  60. package/dist/chunk-Dze3rakg.js +42 -0
  61. package/dist/collections/BackendCollectionRegistry.d.ts +13 -0
  62. package/dist/collections/loader.d.ts +5 -0
  63. package/dist/cron/cron-loader.d.ts +17 -0
  64. package/dist/cron/cron-routes.d.ts +14 -0
  65. package/dist/cron/cron-scheduler.d.ts +106 -0
  66. package/dist/cron/cron-store.d.ts +32 -0
  67. package/dist/cron/index.d.ts +6 -0
  68. package/dist/db/interfaces.d.ts +18 -0
  69. package/dist/dist-DMO-zF6D.js +832 -0
  70. package/dist/dist-DMO-zF6D.js.map +1 -0
  71. package/dist/email/index.d.ts +6 -0
  72. package/dist/email/smtp-email-service.d.ts +25 -0
  73. package/dist/email/templates.d.ts +50 -0
  74. package/dist/email/types.d.ts +126 -0
  75. package/dist/env.d.ts +102 -0
  76. package/dist/from-VbwD7xRf.js +3849 -0
  77. package/dist/from-VbwD7xRf.js.map +1 -0
  78. package/dist/functions/define-function.d.ts +50 -0
  79. package/dist/functions/function-loader.d.ts +17 -0
  80. package/dist/functions/function-routes.d.ts +10 -0
  81. package/dist/functions/index.d.ts +5 -0
  82. package/dist/history/history-routes.d.ts +23 -0
  83. package/dist/history/index.d.ts +1 -0
  84. package/dist/index.d.ts +31 -0
  85. package/dist/index.es.js +61322 -0
  86. package/dist/index.es.js.map +1 -0
  87. package/dist/index.umd.js +103633 -0
  88. package/dist/index.umd.js.map +1 -0
  89. package/dist/init/docs.d.ts +4 -0
  90. package/dist/init/health.d.ts +2 -0
  91. package/dist/init/middlewares.d.ts +10 -0
  92. package/dist/init/shutdown.d.ts +11 -0
  93. package/dist/init/storage.d.ts +5 -0
  94. package/dist/init.d.ts +314 -0
  95. package/dist/jws-BqRRaK11.js +616 -0
  96. package/dist/jws-BqRRaK11.js.map +1 -0
  97. package/dist/jwt-BETC8a1J.js +3560 -0
  98. package/dist/jwt-BETC8a1J.js.map +1 -0
  99. package/dist/logger-BYU66ENZ.js +94 -0
  100. package/dist/logger-BYU66ENZ.js.map +1 -0
  101. package/dist/ms-BeBSuOXN.js +125 -0
  102. package/dist/ms-BeBSuOXN.js.map +1 -0
  103. package/dist/multipart-parser-CedBDOeC.js +299 -0
  104. package/dist/multipart-parser-CedBDOeC.js.map +1 -0
  105. package/dist/serve-spa.d.ts +30 -0
  106. package/dist/services/driver-registry.d.ts +78 -0
  107. package/dist/services/routed-realtime-service.d.ts +43 -0
  108. package/dist/services/webhook-service.d.ts +29 -0
  109. package/dist/singleton.d.ts +35 -0
  110. package/dist/src-CB2PIpBe.js +1182 -0
  111. package/dist/src-CB2PIpBe.js.map +1 -0
  112. package/dist/src-DjzOT1kG.js +29746 -0
  113. package/dist/src-DjzOT1kG.js.map +1 -0
  114. package/dist/storage/GCSStorageController.d.ts +43 -0
  115. package/dist/storage/LocalStorageController.d.ts +46 -0
  116. package/dist/storage/S3StorageController.d.ts +36 -0
  117. package/dist/storage/image-transform.d.ts +50 -0
  118. package/dist/storage/index.d.ts +31 -0
  119. package/dist/storage/routes.d.ts +70 -0
  120. package/dist/storage/storage-registry.d.ts +78 -0
  121. package/dist/storage/tus-handler.d.ts +53 -0
  122. package/dist/storage/types.d.ts +128 -0
  123. package/dist/types/index.d.ts +11 -0
  124. package/dist/utils/dev-port.d.ts +37 -0
  125. package/dist/utils/logger.d.ts +31 -0
  126. package/dist/utils/logging.d.ts +9 -0
  127. package/dist/utils/request-id.d.ts +4 -0
  128. package/dist/utils/request-logger.d.ts +19 -0
  129. package/dist/utils/sql.d.ts +27 -0
  130. package/package.json +22 -14
  131. package/src/api/graphql/graphql-schema-generator.ts +5 -5
  132. package/src/api/openapi-generator.ts +2 -2
  133. package/src/api/rest/api-generator.ts +44 -123
  134. package/src/api/rest/query-parser.ts +6 -23
  135. package/src/api/types.ts +2 -2
  136. package/src/auth/auth-hooks.ts +1 -1
  137. package/src/auth/builtin-auth-adapter.ts +1 -2
  138. package/src/auth/interfaces.ts +11 -2
  139. package/src/auth/middleware.ts +4 -4
  140. package/src/auth/routes.ts +1 -2
  141. package/src/functions/define-function.ts +59 -0
  142. package/src/functions/function-loader.ts +16 -0
  143. package/src/functions/index.ts +2 -0
  144. package/src/index.ts +70 -37
  145. package/src/init.ts +78 -21
  146. package/src/storage/GCSStorageController.ts +334 -0
  147. package/src/storage/index.ts +9 -3
  148. package/src/storage/routes.ts +191 -23
  149. package/src/storage/tus-handler.ts +16 -4
  150. package/src/storage/types.ts +25 -3
  151. package/test/api-generator.test.ts +1 -1
  152. package/test/auth-config-types.test.ts +40 -0
  153. package/test/define-function.test.ts +45 -0
  154. package/test/storage-routes.test.ts +160 -0
  155. package/test/backend-hooks-data.test.ts +0 -477
@@ -0,0 +1,27 @@
1
+ import { SQL } from "drizzle-orm";
2
+ /**
3
+ * Returns a SQL chunk calling `auth.uid()` — the current user's ID.
4
+ * This is a Supabase-style helper function created in the `auth` schema
5
+ * that reads `app.user_id` set per-transaction by `withAuth()`.
6
+ *
7
+ * @example
8
+ * sql`${table.user_id} = ${authUid()}`
9
+ */
10
+ export declare const authUid: () => SQL;
11
+ /**
12
+ * Returns a SQL chunk calling `auth.roles()` — the current user's roles
13
+ * as a comma-separated string.
14
+ * Reads `app.user_roles` set per-transaction by `withAuth()`.
15
+ *
16
+ * @example
17
+ * sql`auth.roles() ~ 'admin'`
18
+ */
19
+ export declare const authRoles: () => SQL;
20
+ /**
21
+ * Returns a SQL chunk calling `auth.jwt()` — the full JWT claims as JSONB.
22
+ * Reads `app.jwt` set per-transaction by `withAuth()`.
23
+ *
24
+ * @example
25
+ * sql`auth.jwt()->>'sub'`
26
+ */
27
+ export declare const authJwt: () => SQL;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/server-core",
3
3
  "type": "module",
4
- "version": "0.7.0",
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"
@@ -30,13 +30,6 @@
30
30
  "backend",
31
31
  "database"
32
32
  ],
33
- "scripts": {
34
- "watch": "vite build --watch",
35
- "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json",
36
- "test:lint": "eslint \"src/**\" --quiet",
37
- "test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules\" jest --passWithNoTests --forceExit",
38
- "clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f"
39
- },
40
33
  "exports": {
41
34
  ".": {
42
35
  "types": "./dist/index.d.ts",
@@ -51,10 +44,6 @@
51
44
  "@aws-sdk/s3-request-presigner": "^3.1068.0",
52
45
  "@hono/graphql-server": "0.7.0",
53
46
  "@hono/node-server": "2.0.4",
54
- "@rebasepro/client": "workspace:*",
55
- "@rebasepro/common": "workspace:*",
56
- "@rebasepro/types": "workspace:*",
57
- "@rebasepro/utils": "workspace:*",
58
47
  "drizzle-orm": "^0.45.2",
59
48
  "google-auth-library": "^10.7.0",
60
49
  "graphql": "^16.14.2",
@@ -64,7 +53,11 @@
64
53
  "sharp": "^0.35.1",
65
54
  "ts-morph": "28.0.0",
66
55
  "ws": "^8.21.0",
67
- "zod": "^4.4.3"
56
+ "zod": "^4.4.3",
57
+ "@rebasepro/client": "0.8.0",
58
+ "@rebasepro/common": "0.8.0",
59
+ "@rebasepro/types": "0.8.0",
60
+ "@rebasepro/utils": "0.8.0"
68
61
  },
69
62
  "devDependencies": {
70
63
  "@types/jest": "^30.0.0",
@@ -81,8 +74,23 @@
81
74
  "typescript": "^6.0.3",
82
75
  "vite": "^8.0.16"
83
76
  },
77
+ "peerDependencies": {
78
+ "@google-cloud/storage": "^7.0.0"
79
+ },
80
+ "peerDependenciesMeta": {
81
+ "@google-cloud/storage": {
82
+ "optional": true
83
+ }
84
+ },
84
85
  "gitHead": "d935eefa5aa8d1009a2398cfac2c1e4ee9aeb6b6",
85
86
  "publishConfig": {
86
87
  "access": "public"
88
+ },
89
+ "scripts": {
90
+ "watch": "vite build --watch",
91
+ "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json",
92
+ "test:lint": "eslint \"src/**\" --quiet",
93
+ "test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules\" jest --passWithNoTests --forceExit",
94
+ "clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f"
87
95
  }
88
- }
96
+ }
@@ -11,7 +11,7 @@ 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
15
  import { isOperationAllowed, type ApiKeyOperation } from "../../auth/api-keys/api-key-permission-guard";
16
16
  import type { ApiKeyMasked } from "../../auth/api-keys/api-key-types";
17
17
 
@@ -219,7 +219,7 @@ export class GraphQLSchemaGenerator {
219
219
  const ds = ctx?.get?.("driver") as DataDriver | undefined ?? (ctx as Record<string, unknown> | undefined)?.driver as DataDriver | undefined;
220
220
  if (!ds) throw new Error("Scoped driver not available");
221
221
  const entity = await ds.fetchEntity({
222
- path: collection.slug,
222
+ path: getCollectionDataPath(collection),
223
223
  entityId: args.id,
224
224
  collection
225
225
  });
@@ -256,7 +256,7 @@ defaultValue: 0 },
256
256
  }
257
257
  }
258
258
  const entities = await ds.fetchCollection({
259
- path: collection.slug,
259
+ path: getCollectionDataPath(collection),
260
260
  collection,
261
261
  filter,
262
262
  limit: args.limit,
@@ -321,7 +321,7 @@ defaultValue: 0 },
321
321
  const ds = ctx?.get?.("driver") as DataDriver | undefined ?? (ctx as Record<string, unknown> | undefined)?.driver as DataDriver | undefined;
322
322
  if (!ds) throw new Error("Scoped driver not available");
323
323
  const entity = await ds.saveEntity({
324
- path: collection.slug,
324
+ path: getCollectionDataPath(collection),
325
325
  entityId: args.id,
326
326
  values: args.input,
327
327
  collection,
@@ -344,7 +344,7 @@ defaultValue: 0 },
344
344
  const ds = ctx?.get?.("driver") as DataDriver | undefined ?? (ctx as Record<string, unknown> | undefined)?.driver as DataDriver | undefined;
345
345
  if (!ds) throw new Error("Scoped driver not available");
346
346
  const existingEntity = await ds.fetchEntity({
347
- path: collection.slug,
347
+ path: getCollectionDataPath(collection),
348
348
  entityId: args.id,
349
349
  collection
350
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
  }
@@ -1,11 +1,11 @@
1
1
  import { Hono } from "hono";
2
- import { AuthAdapter, DataDriver, Entity, EntityCollection, FetchCollectionProps, DataHooks, BackendHookContext, RestFetchService } from "@rebasepro/types";
2
+ import { AuthAdapter, DataDriver, Entity, EntityCollection, RestFetchService, getCollectionDataPath } from "@rebasepro/types";
3
3
  import { QueryOptions, HonoEnv } from "../types";
4
4
  import { ApiError, isRebaseApiError } from "../errors";
5
5
  import { parseQueryOptions } from "./query-parser";
6
6
  import { httpMethodToOperation, isOperationAllowed } from "../../auth/api-keys/api-key-permission-guard";
7
7
  import type { ApiKeyMasked } from "../../auth/api-keys/api-key-types";
8
- import { logger } from "../../utils/logger";
8
+
9
9
 
10
10
 
11
11
  /**
@@ -16,31 +16,21 @@ export class RestApiGenerator {
16
16
  private collections: EntityCollection[];
17
17
  private router: Hono<HonoEnv>;
18
18
  private driver: DataDriver;
19
- private dataHooks?: DataHooks;
19
+
20
20
  private authAdapter?: AuthAdapter;
21
21
 
22
22
  constructor(
23
23
  collections: EntityCollection[],
24
24
  driver: DataDriver,
25
- dataHooks?: DataHooks,
26
25
  authAdapter?: AuthAdapter
27
26
  ) {
28
27
  this.collections = collections;
29
28
  this.driver = driver;
30
- this.dataHooks = dataHooks;
31
29
  this.authAdapter = authAdapter;
32
30
  this.router = new Hono<HonoEnv>();
33
31
  }
34
32
 
35
- /** Build a BackendHookContext from a Hono context */
36
- private buildHookContext(c: { get: (key: string) => unknown }, method: BackendHookContext["method"]): BackendHookContext {
37
- const user = c.get("user") as { userId: string; roles?: string[] } | undefined;
38
- return {
39
- requestUser: user ? { userId: user.userId,
40
- roles: user.roles ?? [] } : undefined,
41
- method
42
- };
43
- }
33
+
44
34
 
45
35
  /**
46
36
  * Generate REST routes using existing DataDriver
@@ -124,7 +114,7 @@ roles: user.roles ?? [] } : undefined,
124
114
 
125
115
  const driver = this.getScopedDriver(c);
126
116
  const fetchService = this.getFetchService(driver);
127
- const hookCtx = this.buildHookContext(c, "GET");
117
+
128
118
 
129
119
  // Use include-aware path when available
130
120
  if (fetchService) {
@@ -132,7 +122,7 @@ roles: user.roles ?? [] } : undefined,
132
122
  let entities = await fetchService.fetchCollectionForRest(
133
123
  collectionPath,
134
124
  {
135
- filter: queryOptions.where as FetchCollectionProps["filter"],
125
+ filter: queryOptions.where,
136
126
  limit: queryOptions.limit,
137
127
  offset: queryOptions.offset,
138
128
  orderBy: queryOptions.orderBy?.[0]?.field,
@@ -143,7 +133,7 @@ roles: user.roles ?? [] } : undefined,
143
133
  queryOptions.include
144
134
  );
145
135
 
146
- entities = await this.applyAfterReadBatch(collection.slug, entities, hookCtx);
136
+
147
137
 
148
138
  const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
149
139
 
@@ -161,7 +151,7 @@ roles: user.roles ?? [] } : undefined,
161
151
  // Fallback path
162
152
  let entities = await this.fetchRawCollection(driver, resolvedCollection, queryOptions, searchString);
163
153
 
164
- entities = await this.applyAfterReadBatch(collection.slug, entities, hookCtx);
154
+
165
155
 
166
156
  const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
167
157
 
@@ -184,7 +174,7 @@ roles: user.roles ?? [] } : undefined,
184
174
  const queryOptions = parseQueryOptions(queryDict);
185
175
  const driver = this.getScopedDriver(c);
186
176
  const fetchService = this.getFetchService(driver);
187
- const hookCtx = this.buildHookContext(c, "GET");
177
+
188
178
 
189
179
  // Use include-aware path when available
190
180
  if (fetchService) {
@@ -199,10 +189,7 @@ roles: user.roles ?? [] } : undefined,
199
189
  throw ApiError.notFound("Entity not found");
200
190
  }
201
191
 
202
- entity = await this.applyAfterRead(collection.slug, entity, hookCtx);
203
- if (!entity) {
204
- throw ApiError.notFound("Entity not found");
205
- }
192
+
206
193
 
207
194
  return c.json(entity);
208
195
  }
@@ -214,10 +201,7 @@ roles: user.roles ?? [] } : undefined,
214
201
  throw ApiError.notFound("Entity not found");
215
202
  }
216
203
 
217
- entity = await this.applyAfterRead(collection.slug, entity, hookCtx);
218
- if (!entity) {
219
- throw ApiError.notFound("Entity not found");
220
- }
204
+
221
205
 
222
206
  return c.json(entity);
223
207
  });
@@ -228,13 +212,11 @@ roles: user.roles ?? [] } : undefined,
228
212
  this.enforceApiKeyPermission(c, collection.slug);
229
213
  const driver = this.getScopedDriver(c);
230
214
  const path = collection.slug;
231
- const hookCtx = this.buildHookContext(c, "POST");
215
+
232
216
 
233
217
  let body = await c.req.json().catch(() => ({}));
234
218
 
235
- if (this.dataHooks?.beforeSave) {
236
- body = await this.dataHooks.beforeSave(path, body, undefined, hookCtx);
237
- }
219
+
238
220
 
239
221
  const isAuth = collection.auth;
240
222
  const isAuthCollection = isAuth === true || (isAuth && typeof isAuth === "object" && isAuth.enabled === true);
@@ -263,11 +245,7 @@ values: entity.values as Record<string, unknown> },
263
245
 
264
246
  const response = this.formatResponse(entity) as Record<string, unknown>;
265
247
 
266
- if (this.dataHooks?.afterSave) {
267
- Promise.resolve(this.dataHooks.afterSave(path, response, hookCtx)).catch(err => {
268
- logger.error("[BackendHooks] data.afterSave error", { error: err instanceof Error ? err.message : err });
269
- });
270
- }
248
+
271
249
 
272
250
  return c.json({
273
251
  ...response,
@@ -285,11 +263,7 @@ values: entity.values as Record<string, unknown> },
285
263
 
286
264
  const response = this.formatResponse(entity);
287
265
 
288
- if (this.dataHooks?.afterSave) {
289
- Promise.resolve(this.dataHooks.afterSave(path, response as Record<string, unknown>, hookCtx)).catch(err => {
290
- logger.error("[BackendHooks] data.afterSave error", { error: err instanceof Error ? err.message : err });
291
- });
292
- }
266
+
293
267
 
294
268
  return c.json(response, 201);
295
269
  } catch (error) {
@@ -315,10 +289,10 @@ values: entity.values as Record<string, unknown> },
315
289
  this.enforceApiKeyPermission(c, collection.slug);
316
290
  const id = c.req.param("id");
317
291
  const driver = this.getScopedDriver(c);
318
- const hookCtx = this.buildHookContext(c, "PUT");
292
+
319
293
 
320
294
  const existingEntity = await driver.fetchEntity({
321
- path: collection.slug,
295
+ path: getCollectionDataPath(collection),
322
296
  entityId: String(id),
323
297
  collection: resolvedCollection
324
298
  });
@@ -329,12 +303,10 @@ values: entity.values as Record<string, unknown> },
329
303
 
330
304
  let body = await c.req.json().catch(() => ({}));
331
305
 
332
- if (this.dataHooks?.beforeSave) {
333
- body = await this.dataHooks.beforeSave(collection.slug, body, String(id), hookCtx);
334
- }
306
+
335
307
 
336
308
  const entity = await driver.saveEntity({
337
- path: collection.slug,
309
+ path: getCollectionDataPath(collection),
338
310
  entityId: String(id),
339
311
  values: body,
340
312
  collection: resolvedCollection,
@@ -343,11 +315,7 @@ values: entity.values as Record<string, unknown> },
343
315
 
344
316
  const response = this.formatResponse(entity);
345
317
 
346
- if (this.dataHooks?.afterSave) {
347
- Promise.resolve(this.dataHooks.afterSave(collection.slug, response as Record<string, unknown>, hookCtx)).catch(err => {
348
- logger.error("[BackendHooks] data.afterSave error", { error: err instanceof Error ? err.message : err });
349
- });
350
- }
318
+
351
319
 
352
320
  return c.json(response);
353
321
  } catch (error) {
@@ -371,10 +339,10 @@ values: entity.values as Record<string, unknown> },
371
339
  this.enforceApiKeyPermission(c, collection.slug);
372
340
  const id = c.req.param("id");
373
341
  const driver = this.getScopedDriver(c);
374
- const hookCtx = this.buildHookContext(c, "DELETE");
342
+
375
343
 
376
344
  const existingEntity = await driver.fetchEntity({
377
- path: collection.slug,
345
+ path: getCollectionDataPath(collection),
378
346
  entityId: String(id),
379
347
  collection: resolvedCollection
380
348
  });
@@ -383,20 +351,14 @@ values: entity.values as Record<string, unknown> },
383
351
  throw ApiError.notFound("Entity not found");
384
352
  }
385
353
 
386
- if (this.dataHooks?.beforeDelete) {
387
- await this.dataHooks.beforeDelete(collection.slug, String(id), hookCtx);
388
- }
354
+
389
355
 
390
356
  await driver.deleteEntity({
391
357
  entity: existingEntity,
392
358
  collection: resolvedCollection
393
359
  });
394
360
 
395
- if (this.dataHooks?.afterDelete) {
396
- Promise.resolve(this.dataHooks.afterDelete(collection.slug, String(id), hookCtx)).catch(err => {
397
- logger.error("[BackendHooks] data.afterDelete error", { error: err instanceof Error ? err.message : err });
398
- });
399
- }
361
+
400
362
 
401
363
  return new Response(null, { status: 204 });
402
364
  });
@@ -464,7 +426,7 @@ entityId };
464
426
 
465
427
  this.enforceApiKeyPermission(c, c.req.param("parent"));
466
428
 
467
- const hookCtx = this.buildHookContext(c, "GET");
429
+
468
430
 
469
431
  if (parsed.entityId === "count") {
470
432
  // GET /parent/:parentId/child/count — count child entities
@@ -474,7 +436,7 @@ entityId };
474
436
 
475
437
  const total = driver.countEntities ? await driver.countEntities({
476
438
  path: parsed.collectionPath,
477
- filter: queryOptions.where as FetchCollectionProps["filter"],
439
+ filter: queryOptions.where,
478
440
  searchString
479
441
  }) : 0;
480
442
 
@@ -487,11 +449,7 @@ entityId };
487
449
  });
488
450
  if (!entity) throw ApiError.notFound("Entity not found");
489
451
 
490
- const flatResult = this.flattenEntity(entity);
491
- const hookResult = await this.applyAfterRead(c.req.param("parent"), flatResult, hookCtx);
492
- if (!hookResult) throw ApiError.notFound("Entity not found");
493
-
494
- return c.json(hookResult);
452
+ return c.json(this.flattenEntity(entity));
495
453
  } else {
496
454
  // GET /parent/:parentId/child — list entities
497
455
  const queryDict = c.req.queries();
@@ -499,19 +457,18 @@ entityId };
499
457
  const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : undefined;
500
458
  const entities = await driver.fetchCollection({
501
459
  path: parsed.collectionPath,
502
- filter: queryOptions.where as FetchCollectionProps["filter"],
460
+ filter: queryOptions.where,
503
461
  limit: queryOptions.limit,
504
462
  orderBy: queryOptions.orderBy?.[0]?.field,
505
463
  order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
506
464
  searchString
507
465
  });
508
466
 
509
- let flatEntities = entities.map(e => this.flattenEntity(e));
510
- flatEntities = await this.applyAfterReadBatch(c.req.param("parent"), flatEntities, hookCtx);
467
+ const flatEntities = entities.map(e => this.flattenEntity(e));
511
468
 
512
469
  const total = driver.countEntities ? await driver.countEntities({
513
470
  path: parsed.collectionPath,
514
- filter: queryOptions.where as FetchCollectionProps["filter"],
471
+ filter: queryOptions.where,
515
472
  searchString
516
473
  }) : flatEntities.length;
517
474
 
@@ -536,14 +493,12 @@ entityId };
536
493
  if (!parsed || parsed.entityId) return next();
537
494
 
538
495
  const driver = this.getScopedDriver(c);
539
- const hookCtx = this.buildHookContext(c, "POST");
496
+
540
497
 
541
498
  this.enforceApiKeyPermission(c, c.req.param("parent"));
542
499
  let body = await c.req.json().catch(() => ({}));
543
500
 
544
- if (this.dataHooks?.beforeSave) {
545
- body = await this.dataHooks.beforeSave(parsed.collectionPath, body, undefined, hookCtx);
546
- }
501
+
547
502
 
548
503
  const entity = await driver.saveEntity({
549
504
  path: parsed.collectionPath,
@@ -553,11 +508,7 @@ entityId };
553
508
 
554
509
  const response = this.formatResponse(entity);
555
510
 
556
- if (this.dataHooks?.afterSave) {
557
- Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response as Record<string, unknown>, hookCtx)).catch(err => {
558
- logger.error("[BackendHooks] data.afterSave error", { error: err instanceof Error ? err.message : err });
559
- });
560
- }
511
+
561
512
 
562
513
  return c.json(response, 201);
563
514
  });
@@ -571,15 +522,13 @@ entityId };
571
522
  if (!parsed || !parsed.entityId) return next();
572
523
 
573
524
  const driver = this.getScopedDriver(c);
574
- const hookCtx = this.buildHookContext(c, "PUT");
525
+
575
526
 
576
527
  this.enforceApiKeyPermission(c, c.req.param("parent"));
577
528
 
578
529
  let body = await c.req.json().catch(() => ({}));
579
530
 
580
- if (this.dataHooks?.beforeSave) {
581
- body = await this.dataHooks.beforeSave(parsed.collectionPath, body, parsed.entityId, hookCtx);
582
- }
531
+
583
532
 
584
533
  const entity = await driver.saveEntity({
585
534
  path: parsed.collectionPath,
@@ -590,11 +539,7 @@ entityId };
590
539
 
591
540
  const response = this.formatResponse(entity);
592
541
 
593
- if (this.dataHooks?.afterSave) {
594
- Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response as Record<string, unknown>, hookCtx)).catch(err => {
595
- logger.error("[BackendHooks] data.afterSave error", { error: err instanceof Error ? err.message : err });
596
- });
597
- }
542
+
598
543
 
599
544
  return c.json(response);
600
545
  });
@@ -608,7 +553,7 @@ entityId };
608
553
  if (!parsed || !parsed.entityId) return next();
609
554
 
610
555
  const driver = this.getScopedDriver(c);
611
- const hookCtx = this.buildHookContext(c, "DELETE");
556
+
612
557
 
613
558
  this.enforceApiKeyPermission(c, c.req.param("parent"));
614
559
 
@@ -619,17 +564,11 @@ entityId };
619
564
 
620
565
  if (!existingEntity) throw ApiError.notFound("Entity not found");
621
566
 
622
- if (this.dataHooks?.beforeDelete) {
623
- await this.dataHooks.beforeDelete(parsed.collectionPath, parsed.entityId, hookCtx);
624
- }
567
+
625
568
 
626
569
  await driver.deleteEntity({ entity: existingEntity });
627
570
 
628
- if (this.dataHooks?.afterDelete) {
629
- Promise.resolve(this.dataHooks.afterDelete(parsed.collectionPath, parsed.entityId, hookCtx)).catch(err => {
630
- logger.error("[BackendHooks] data.afterDelete error", { error: err instanceof Error ? err.message : err });
631
- });
632
- }
571
+
633
572
 
634
573
  return new Response(null, { status: 204 });
635
574
  });
@@ -686,9 +625,9 @@ entityId };
686
625
  */
687
626
  private async fetchRawCollection(driver: DataDriver, collection: EntityCollection, queryOptions: QueryOptions, searchString?: string) {
688
627
  const entities = await driver.fetchCollection({
689
- path: collection.slug,
628
+ path: getCollectionDataPath(collection),
690
629
  collection,
691
- filter: queryOptions.where as FetchCollectionProps["filter"],
630
+ filter: queryOptions.where,
692
631
  limit: queryOptions.limit,
693
632
  orderBy: queryOptions.orderBy?.[0]?.field,
694
633
  order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
@@ -705,9 +644,9 @@ entityId };
705
644
  */
706
645
  private async countRawEntities(driver: DataDriver, collection: EntityCollection, queryOptions: QueryOptions, searchString?: string): Promise<number> {
707
646
  return driver.countEntities ? await driver.countEntities({
708
- path: collection.slug,
647
+ path: getCollectionDataPath(collection),
709
648
  collection,
710
- filter: queryOptions.where as FetchCollectionProps["filter"],
649
+ filter: queryOptions.where,
711
650
  searchString
712
651
  }) : 0;
713
652
  }
@@ -717,7 +656,7 @@ entityId };
717
656
  */
718
657
  private async fetchRawEntity(driver: DataDriver, collection: EntityCollection, entityId: string) {
719
658
  const entity = await driver.fetchEntity({
720
- path: collection.slug,
659
+ path: getCollectionDataPath(collection),
721
660
  entityId,
722
661
  collection
723
662
  });
@@ -725,23 +664,5 @@ entityId };
725
664
  return entity ? this.flattenEntity(entity) : null;
726
665
  }
727
666
 
728
- /**
729
- * Apply data.afterRead hook to a single entity.
730
- * Returns the transformed entity, or null to filter it out.
731
- */
732
- private async applyAfterRead(slug: string, entity: Record<string, unknown>, ctx: BackendHookContext): Promise<Record<string, unknown> | null> {
733
- if (!this.dataHooks?.afterRead) return entity;
734
- return this.dataHooks.afterRead(slug, entity, ctx);
735
- }
736
667
 
737
- /**
738
- * Apply data.afterRead hook to an array of entities, filtering out nulls.
739
- */
740
- private async applyAfterReadBatch(slug: string, entities: Record<string, unknown>[], ctx: BackendHookContext): Promise<Record<string, unknown>[]> {
741
- if (!this.dataHooks?.afterRead) return entities;
742
- const results = await Promise.all(
743
- entities.map(e => this.applyAfterRead(slug, e, ctx))
744
- );
745
- return results.filter((e): e is Record<string, unknown> => e !== null);
746
- }
747
668
  }
@@ -1,25 +1,8 @@
1
- import type { VectorSearchParams, LogicalCondition, FilterCondition } from "@rebasepro/types";
1
+ import type { VectorSearchParams, LogicalCondition, FilterCondition, WhereFilterOp } from "@rebasepro/types";
2
+ import { toCanonicalOp } from "@rebasepro/types";
3
+ export const mapOperator = (op: string) => toCanonicalOp(op) ?? null;
2
4
  import { QueryOptions } from "../types";
3
5
 
4
- /**
5
- * Map PostgREST-style operators to Rebase WhereFilterOp
6
- */
7
- export function mapOperator(op: string): string | null {
8
- switch (op) {
9
- case "eq": return "==";
10
- case "neq": return "!=";
11
- case "gt": return ">";
12
- case "gte": return ">=";
13
- case "lt": return "<";
14
- case "lte": return "<=";
15
- case "in": return "in";
16
- case "nin": return "not-in";
17
- case "cs": return "array-contains";
18
- case "csa": return "array-contains-any";
19
- default: return null;
20
- }
21
- }
22
-
23
6
  function getLastValue(val: unknown): unknown {
24
7
  if (Array.isArray(val)) {
25
8
  return val[val.length - 1];
@@ -60,7 +43,7 @@ value: true };
60
43
  valStr = rest;
61
44
  }
62
45
 
63
- const rebaseOp = (mapOperator(op) || "==") as FilterCondition["operator"];
46
+ const rebaseOp = (toCanonicalOp(op) ?? "==") as FilterCondition["operator"];
64
47
  let parsedVal: unknown = valStr;
65
48
  if (valStr === "true") parsedVal = true;
66
49
  else if (valStr === "false") parsedVal = false;
@@ -158,7 +141,7 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
158
141
  if (reservedQueryKeys.includes(key)) continue;
159
142
 
160
143
  const rawValues = Array.isArray(rawValue) ? rawValue : [rawValue];
161
- const conditions: [string, unknown][] = [];
144
+ const conditions: [WhereFilterOp, unknown][] = [];
162
145
 
163
146
  for (const value of rawValues) {
164
147
  if (typeof value === "string") {
@@ -166,7 +149,7 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
166
149
  if (parts.length >= 2) {
167
150
  const op = parts[0];
168
151
  const val = parts.slice(1).join(".");
169
- const rebaseOp = mapOperator(op);
152
+ const rebaseOp = toCanonicalOp(op);
170
153
 
171
154
  if (rebaseOp) {
172
155
  let parsedVal: string | number | boolean | null | (string | number | boolean | null)[] = val;
package/src/api/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { EntityCollection, VectorSearchParams, LogicalCondition } from "@rebasepro/types";
1
+ import { EntityCollection, VectorSearchParams, LogicalCondition, FilterValues } from "@rebasepro/types";
2
2
  import { AuthResult } from "../auth/middleware";
3
3
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
4
4
  import { DataDriver } from "@rebasepro/types";
@@ -84,7 +84,7 @@ export interface ApiResponse<T = unknown> {
84
84
  export interface QueryOptions {
85
85
  limit?: number;
86
86
  offset?: number;
87
- where?: Record<string, unknown>;
87
+ where?: FilterValues<string>;
88
88
  logical?: LogicalCondition;
89
89
  orderBy?: Array<{ field: string; direction: "asc" | "desc" }>;
90
90
  include?: string[];
@@ -190,7 +190,7 @@ export interface AuthHooks {
190
190
  * response and returns a (potentially enriched) response.
191
191
  *
192
192
  * Use cases:
193
- * - Inject tokens from external auth systems (Firebase Custom Tokens, etc.)
193
+ * - Inject tokens from external auth systems (custom provider tokens, etc.)
194
194
  * - Add project-specific metadata to the response
195
195
  * - Enrich the user object with data from external sources
196
196
  *
@@ -56,8 +56,7 @@ export interface BuiltinAuthAdapterConfig {
56
56
  /** Default role to assign to new users. */
57
57
  defaultRole?: string;
58
58
  /** OAuth providers to register. */
59
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
- oauthProviders?: OAuthProvider<any>[];
59
+ oauthProviders?: OAuthProvider<unknown>[];
61
60
  /** Static service key for server-to-server auth. */
62
61
  serviceKey?: string;
63
62
  /** Auth hooks for customizing password, credentials, lifecycle, etc. */