@rebasepro/server-core 0.4.0 → 0.6.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 +62 -25
- package/dist/{server-core/src/api → api}/errors.d.ts +20 -2
- package/dist/{server-core/src/api → api}/rest/api-generator.d.ts +8 -2
- package/dist/{server-core/src/api → api}/types.d.ts +2 -0
- package/dist/auth/admin-user-ops.d.ts +79 -0
- package/dist/{server-core/src/auth → auth}/api-keys/index.d.ts +2 -2
- package/dist/{server-core/src/auth → auth}/auth-hooks.d.ts +37 -9
- package/dist/{server-core/src/auth → auth}/builtin-auth-adapter.d.ts +4 -4
- package/dist/{server-core/src/auth → auth}/index.d.ts +5 -2
- package/dist/{server-core/src/auth → auth}/jwt.d.ts +10 -0
- package/dist/auth/mfa-routes.d.ts +6 -0
- package/dist/auth/reset-password-admin.d.ts +29 -0
- package/dist/auth/session-routes.d.ts +25 -0
- package/dist/backend-CIxN4FVm.js +15 -0
- package/dist/backend-CIxN4FVm.js.map +1 -0
- package/dist/chunk-Dze3rakg.js +42 -0
- package/dist/{server-core/src/cron → cron}/cron-scheduler.d.ts +1 -1
- package/dist/dist-CZKP-Xz4.js +832 -0
- package/dist/dist-CZKP-Xz4.js.map +1 -0
- package/dist/env.d.ts +102 -0
- package/dist/from-VbwD7xRf.js +3849 -0
- package/dist/from-VbwD7xRf.js.map +1 -0
- package/dist/{server-core/src/index.d.ts → index.d.ts} +1 -0
- package/dist/index.es.js +60871 -50424
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +73151 -51256
- package/dist/index.umd.js.map +1 -1
- package/dist/init/docs.d.ts +4 -0
- package/dist/init/health.d.ts +2 -0
- package/dist/init/middlewares.d.ts +10 -0
- package/dist/init/shutdown.d.ts +11 -0
- package/dist/init/storage.d.ts +5 -0
- package/dist/{server-core/src/init.d.ts → init.d.ts} +4 -4
- package/dist/jwt-DHcQRGC3.js +4168 -0
- package/dist/jwt-DHcQRGC3.js.map +1 -0
- package/dist/logger-BYU66ENZ.js +94 -0
- package/dist/logger-BYU66ENZ.js.map +1 -0
- package/dist/ms-BeBSuOXN.js +125 -0
- package/dist/ms-BeBSuOXN.js.map +1 -0
- package/dist/multipart-parser-CedBDOeC.js +299 -0
- package/dist/multipart-parser-CedBDOeC.js.map +1 -0
- package/dist/src-COaj0G3P.js +1182 -0
- package/dist/src-COaj0G3P.js.map +1 -0
- package/dist/{server-core/src/storage → storage}/image-transform.d.ts +3 -1
- package/dist/utils/request-id.d.ts +4 -0
- package/package.json +29 -29
- package/src/api/collections_for_test/callbacks_test_collection.ts +7 -6
- package/src/api/errors.ts +95 -20
- package/src/api/graphql/graphql-schema-generator.ts +15 -10
- package/src/api/logs-routes.ts +5 -2
- package/src/api/openapi-generator.ts +1 -1
- package/src/api/rest/api-generator-count.test.ts +21 -10
- package/src/api/rest/api-generator.ts +151 -36
- package/src/api/rest/query-parser.ts +18 -12
- package/src/api/server.ts +13 -2
- package/src/api/types.ts +2 -0
- package/src/auth/adapter-middleware.ts +17 -10
- package/src/auth/admin-user-ops.ts +236 -0
- package/src/auth/api-keys/api-key-middleware.ts +15 -9
- package/src/auth/api-keys/api-key-permission-guard.ts +1 -1
- package/src/auth/api-keys/api-key-routes.ts +3 -3
- package/src/auth/api-keys/api-key-store.ts +14 -8
- package/src/auth/api-keys/index.ts +2 -2
- package/src/auth/apple-oauth.ts +4 -3
- package/src/auth/auth-hooks.ts +50 -10
- package/src/auth/bitbucket-oauth.ts +5 -4
- package/src/auth/builtin-auth-adapter.ts +64 -54
- package/src/auth/custom-auth-adapter.ts +4 -5
- package/src/auth/discord-oauth.ts +5 -4
- package/src/auth/facebook-oauth.ts +5 -4
- package/src/auth/github-oauth.ts +6 -5
- package/src/auth/gitlab-oauth.ts +5 -4
- package/src/auth/google-oauth.ts +2 -1
- package/src/auth/index.ts +6 -2
- package/src/auth/jwt.ts +15 -4
- package/src/auth/linkedin-oauth.ts +4 -3
- package/src/auth/mfa-routes.ts +299 -0
- package/src/auth/mfa.ts +2 -1
- package/src/auth/microsoft-oauth.ts +5 -4
- package/src/auth/middleware.ts +5 -4
- package/src/auth/rate-limiter.ts +1 -1
- package/src/auth/reset-password-admin.ts +144 -0
- package/src/auth/rls-scope.ts +1 -1
- package/src/auth/routes.ts +52 -600
- package/src/auth/session-routes.ts +341 -0
- package/src/auth/slack-oauth.ts +6 -5
- package/src/auth/spotify-oauth.ts +5 -4
- package/src/auth/twitter-oauth.ts +4 -3
- package/src/collections/loader.ts +5 -4
- package/src/cron/cron-scheduler.test.ts +23 -11
- package/src/cron/cron-scheduler.ts +14 -8
- package/src/cron/cron-store.ts +1 -1
- package/src/email/smtp-email-service.ts +3 -2
- package/src/env.ts +11 -10
- package/src/index.ts +1 -0
- package/src/init/docs.ts +47 -0
- package/src/init/health.ts +37 -0
- package/src/init/middlewares.ts +61 -0
- package/src/init/shutdown.ts +56 -0
- package/src/init/storage.ts +57 -0
- package/src/init.ts +93 -295
- package/src/serve-spa.ts +12 -5
- package/src/services/driver-registry.ts +4 -3
- package/src/storage/S3StorageController.ts +1 -2
- package/src/storage/image-transform.ts +25 -10
- package/src/storage/routes.ts +30 -11
- package/src/storage/storage-registry.ts +4 -3
- package/src/storage/tus-handler.ts +12 -12
- package/src/utils/request-id.ts +40 -0
- package/src/utils/request-logger.ts +6 -0
- package/test/api-generator.test.ts +90 -2
- package/test/api-key-permission-guard.test.ts +24 -12
- package/test/auth-routes.test.ts +5 -3
- package/test/backend-hooks-data.test.ts +99 -30
- package/test/custom-auth-adapter.test.ts +16 -15
- package/test/email-templates.test.ts +10 -5
- package/test/env.test.ts +10 -10
- package/test/function-loader.test.ts +7 -4
- package/test/graphql-schema-generator.test.ts +554 -0
- package/test/jwt-security.test.ts +1 -1
- package/test/query-parser.test.ts +0 -1
- package/test/reset-password-admin.test.ts +113 -0
- package/test/singleton.test.ts +5 -5
- package/test/smtp-email-service.test.ts +21 -21
- package/test/webhook-service.test.ts +22 -11
- package/tsconfig.json +2 -0
- package/tsconfig.prod.json +3 -0
- package/vite.config.ts +5 -5
- package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
- package/dist/common/src/collections/default-collections.d.ts +0 -9
- package/dist/common/src/collections/index.d.ts +0 -2
- package/dist/common/src/data/buildRebaseData.d.ts +0 -14
- package/dist/common/src/data/query_builder.d.ts +0 -55
- package/dist/common/src/index.d.ts +0 -4
- package/dist/common/src/util/builders.d.ts +0 -57
- package/dist/common/src/util/callbacks.d.ts +0 -6
- package/dist/common/src/util/collections.d.ts +0 -11
- package/dist/common/src/util/common.d.ts +0 -2
- package/dist/common/src/util/conditions.d.ts +0 -26
- package/dist/common/src/util/entities.d.ts +0 -58
- package/dist/common/src/util/enums.d.ts +0 -3
- package/dist/common/src/util/index.d.ts +0 -16
- package/dist/common/src/util/navigation_from_path.d.ts +0 -34
- package/dist/common/src/util/navigation_utils.d.ts +0 -20
- package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
- package/dist/common/src/util/paths.d.ts +0 -14
- package/dist/common/src/util/permissions.d.ts +0 -6
- package/dist/common/src/util/references.d.ts +0 -2
- package/dist/common/src/util/relations.d.ts +0 -22
- package/dist/common/src/util/resolutions.d.ts +0 -72
- package/dist/common/src/util/storage.d.ts +0 -24
- package/dist/index-Cr1D21av.js +0 -49
- package/dist/index-Cr1D21av.js.map +0 -1
- package/dist/server-core/src/auth/admin-routes.d.ts +0 -27
- package/dist/server-core/src/env.d.ts +0 -137
- package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
- package/dist/types/src/controllers/auth.d.ts +0 -104
- package/dist/types/src/controllers/client.d.ts +0 -168
- package/dist/types/src/controllers/collection_registry.d.ts +0 -46
- package/dist/types/src/controllers/customization_controller.d.ts +0 -60
- package/dist/types/src/controllers/data.d.ts +0 -207
- package/dist/types/src/controllers/data_driver.d.ts +0 -218
- package/dist/types/src/controllers/database_admin.d.ts +0 -11
- package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
- package/dist/types/src/controllers/effective_role.d.ts +0 -4
- package/dist/types/src/controllers/email.d.ts +0 -36
- package/dist/types/src/controllers/index.d.ts +0 -18
- package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
- package/dist/types/src/controllers/navigation.d.ts +0 -225
- package/dist/types/src/controllers/registry.d.ts +0 -63
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
- package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
- package/dist/types/src/controllers/snackbar.d.ts +0 -24
- package/dist/types/src/controllers/storage.d.ts +0 -171
- package/dist/types/src/index.d.ts +0 -4
- package/dist/types/src/rebase_context.d.ts +0 -122
- package/dist/types/src/types/auth_adapter.d.ts +0 -301
- package/dist/types/src/types/backend.d.ts +0 -536
- package/dist/types/src/types/backend_hooks.d.ts +0 -172
- package/dist/types/src/types/builders.d.ts +0 -15
- package/dist/types/src/types/chips.d.ts +0 -5
- package/dist/types/src/types/collections.d.ts +0 -941
- package/dist/types/src/types/component_ref.d.ts +0 -47
- package/dist/types/src/types/cron.d.ts +0 -102
- package/dist/types/src/types/data_source.d.ts +0 -64
- package/dist/types/src/types/database_adapter.d.ts +0 -94
- package/dist/types/src/types/entities.d.ts +0 -145
- package/dist/types/src/types/entity_actions.d.ts +0 -104
- package/dist/types/src/types/entity_callbacks.d.ts +0 -173
- package/dist/types/src/types/entity_link_builder.d.ts +0 -7
- package/dist/types/src/types/entity_overrides.d.ts +0 -10
- package/dist/types/src/types/entity_views.d.ts +0 -87
- package/dist/types/src/types/export_import.d.ts +0 -21
- package/dist/types/src/types/formex.d.ts +0 -40
- package/dist/types/src/types/index.d.ts +0 -28
- package/dist/types/src/types/locales.d.ts +0 -4
- package/dist/types/src/types/modify_collections.d.ts +0 -5
- package/dist/types/src/types/plugins.d.ts +0 -282
- package/dist/types/src/types/properties.d.ts +0 -1181
- package/dist/types/src/types/property_config.d.ts +0 -74
- package/dist/types/src/types/relations.d.ts +0 -336
- package/dist/types/src/types/slots.d.ts +0 -262
- package/dist/types/src/types/translations.d.ts +0 -900
- package/dist/types/src/types/user_management_delegate.d.ts +0 -86
- package/dist/types/src/types/websockets.d.ts +0 -78
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -50
- package/src/auth/admin-routes.ts +0 -534
- package/test/admin-routes.test.ts +0 -544
- package/test/backend-hooks-admin.test.ts +0 -388
- package/test.ts +0 -6
- /package/dist/{server-core/src/api → api}/ast-schema-editor.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/collections_for_test/callbacks_test_collection.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/graphql/graphql-schema-generator.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/graphql/index.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/index.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/logs-routes.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/openapi-generator.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/rest/index.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/rest/query-parser.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/schema-editor-routes.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/server.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/adapter-middleware.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-middleware.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-permission-guard.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-routes.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-store.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-types.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/apple-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/bitbucket-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/crypto-utils.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/custom-auth-adapter.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/discord-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/facebook-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/github-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/gitlab-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/google-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/interfaces.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/linkedin-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/mfa.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/microsoft-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/middleware.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/password.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/rate-limiter.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/rls-scope.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/routes.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/slack-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/spotify-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/twitter-oauth.d.ts +0 -0
- /package/dist/{server-core/src/collections → collections}/BackendCollectionRegistry.d.ts +0 -0
- /package/dist/{server-core/src/collections → collections}/loader.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/cron-loader.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/cron-routes.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/cron-store.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/index.d.ts +0 -0
- /package/dist/{server-core/src/db → db}/interfaces.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/index.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/smtp-email-service.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/templates.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/types.d.ts +0 -0
- /package/dist/{server-core/src/functions → functions}/function-loader.d.ts +0 -0
- /package/dist/{server-core/src/functions → functions}/function-routes.d.ts +0 -0
- /package/dist/{server-core/src/functions → functions}/index.d.ts +0 -0
- /package/dist/{server-core/src/history → history}/history-routes.d.ts +0 -0
- /package/dist/{server-core/src/history → history}/index.d.ts +0 -0
- /package/dist/{server-core/src/serve-spa.d.ts → serve-spa.d.ts} +0 -0
- /package/dist/{server-core/src/services → services}/driver-registry.d.ts +0 -0
- /package/dist/{server-core/src/services → services}/webhook-service.d.ts +0 -0
- /package/dist/{server-core/src/singleton.d.ts → singleton.d.ts} +0 -0
- /package/dist/{server-core/src/storage → storage}/LocalStorageController.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/S3StorageController.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/index.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/routes.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/storage-registry.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/tus-handler.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/types.d.ts +0 -0
- /package/dist/{server-core/src/types → types}/index.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/dev-port.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/logger.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/logging.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/request-logger.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/sql.d.ts +0 -0
|
@@ -184,8 +184,9 @@ export class GraphQLSchemaGenerator {
|
|
|
184
184
|
id: { type: new GraphQLNonNull(GraphQLString) }
|
|
185
185
|
},
|
|
186
186
|
resolve: async (_, args, context: unknown) => {
|
|
187
|
-
const ctx = context as { driver
|
|
188
|
-
const ds = ctx
|
|
187
|
+
const ctx = context as { driver?: DataDriver } | undefined;
|
|
188
|
+
const ds = ctx?.driver;
|
|
189
|
+
if (!ds) throw new Error("Scoped driver not available");
|
|
189
190
|
const entity = await ds.fetchEntity({
|
|
190
191
|
path: collection.slug,
|
|
191
192
|
entityId: args.id,
|
|
@@ -207,8 +208,9 @@ defaultValue: 0 },
|
|
|
207
208
|
orderBy: { type: GraphQLString }
|
|
208
209
|
},
|
|
209
210
|
resolve: async (_, args, context: unknown) => {
|
|
210
|
-
const ctx = context as { driver
|
|
211
|
-
const ds = ctx
|
|
211
|
+
const ctx = context as { driver?: DataDriver } | undefined;
|
|
212
|
+
const ds = ctx?.driver;
|
|
213
|
+
if (!ds) throw new Error("Scoped driver not available");
|
|
212
214
|
let filter: FetchCollectionProps["filter"] | undefined;
|
|
213
215
|
if (args.where) {
|
|
214
216
|
try {
|
|
@@ -259,8 +261,9 @@ defaultValue: 0 },
|
|
|
259
261
|
input: { type: new GraphQLNonNull(inputType) }
|
|
260
262
|
},
|
|
261
263
|
resolve: async (_, args, context: unknown) => {
|
|
262
|
-
const ctx = context as { driver
|
|
263
|
-
const ds = ctx
|
|
264
|
+
const ctx = context as { driver?: DataDriver } | undefined;
|
|
265
|
+
const ds = ctx?.driver;
|
|
266
|
+
if (!ds) throw new Error("Scoped driver not available");
|
|
264
267
|
const path = collection.slug;
|
|
265
268
|
const entity = await ds.saveEntity({
|
|
266
269
|
path,
|
|
@@ -280,8 +283,9 @@ defaultValue: 0 },
|
|
|
280
283
|
input: { type: new GraphQLNonNull(inputType) }
|
|
281
284
|
},
|
|
282
285
|
resolve: async (_, args, context: unknown) => {
|
|
283
|
-
const ctx = context as { driver
|
|
284
|
-
const ds = ctx
|
|
286
|
+
const ctx = context as { driver?: DataDriver } | undefined;
|
|
287
|
+
const ds = ctx?.driver;
|
|
288
|
+
if (!ds) throw new Error("Scoped driver not available");
|
|
285
289
|
const entity = await ds.saveEntity({
|
|
286
290
|
path: collection.slug,
|
|
287
291
|
entityId: args.id,
|
|
@@ -301,8 +305,9 @@ defaultValue: 0 },
|
|
|
301
305
|
},
|
|
302
306
|
resolve: async (_, args, context: unknown) => {
|
|
303
307
|
try {
|
|
304
|
-
const ctx = context as { driver
|
|
305
|
-
const ds = ctx
|
|
308
|
+
const ctx = context as { driver?: DataDriver } | undefined;
|
|
309
|
+
const ds = ctx?.driver;
|
|
310
|
+
if (!ds) throw new Error("Scoped driver not available");
|
|
306
311
|
const existingEntity = await ds.fetchEntity({
|
|
307
312
|
path: collection.slug,
|
|
308
313
|
entityId: args.id,
|
package/src/api/logs-routes.ts
CHANGED
|
@@ -22,7 +22,8 @@ class LogRingBuffer {
|
|
|
22
22
|
|
|
23
23
|
push(entry: Omit<LogEntry, "id">): void {
|
|
24
24
|
const id = `log_${++this.idCounter}`;
|
|
25
|
-
this.buffer.push({ ...entry,
|
|
25
|
+
this.buffer.push({ ...entry,
|
|
26
|
+
id });
|
|
26
27
|
if (this.buffer.length > this.maxSize) {
|
|
27
28
|
this.buffer.shift();
|
|
28
29
|
}
|
|
@@ -95,11 +96,13 @@ export function logMiddleware(): MiddlewareHandler<HonoEnv> {
|
|
|
95
96
|
const start = Date.now();
|
|
96
97
|
await next();
|
|
97
98
|
const duration = Date.now() - start;
|
|
99
|
+
const reqId = c.get("requestId");
|
|
98
100
|
addLog("info", "api", `${c.req.method} ${c.req.path} ${c.res.status} ${duration}ms`, {
|
|
99
101
|
method: c.req.method,
|
|
100
102
|
path: c.req.path,
|
|
101
103
|
status: c.res.status,
|
|
102
|
-
duration
|
|
104
|
+
duration,
|
|
105
|
+
...(reqId && { requestId: reqId })
|
|
103
106
|
});
|
|
104
107
|
};
|
|
105
108
|
}
|
|
@@ -314,7 +314,7 @@ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResp
|
|
|
314
314
|
};
|
|
315
315
|
|
|
316
316
|
// ── Subcollection routes ──────────────────────────────────────
|
|
317
|
-
const relations =
|
|
317
|
+
const relations = collection.relations;
|
|
318
318
|
if (relations && relations.length > 0) {
|
|
319
319
|
for (const relation of relations) {
|
|
320
320
|
const relationName = relation.relationName;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jest } from "@jest/globals";
|
|
2
|
+
import { Hono } from "hono";
|
|
2
3
|
import { RestApiGenerator } from "./api-generator";
|
|
3
4
|
import type { DataDriver, Entity, EntityCollection, FetchCollectionProps } from "@rebasepro/types";
|
|
4
5
|
|
|
@@ -27,6 +28,21 @@ function createTestCollection(slug: string): EntityCollection {
|
|
|
27
28
|
} as unknown as EntityCollection;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Wraps generated routes in a parent Hono app that injects the driver
|
|
33
|
+
* into context — replicating what auth middleware does in production.
|
|
34
|
+
*/
|
|
35
|
+
function createApp(collections: EntityCollection[], driver: DataDriver): Hono {
|
|
36
|
+
const parent = new Hono();
|
|
37
|
+
parent.use("/*", async (c, next) => {
|
|
38
|
+
c.set("driver", driver);
|
|
39
|
+
await next();
|
|
40
|
+
});
|
|
41
|
+
const generator = new RestApiGenerator(collections, driver);
|
|
42
|
+
parent.route("/", generator.generateRoutes());
|
|
43
|
+
return parent;
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
describe("RestApiGenerator - Count Endpoint", () => {
|
|
31
47
|
let driver: DataDriver;
|
|
32
48
|
let collection: EntityCollection;
|
|
@@ -39,8 +55,7 @@ describe("RestApiGenerator - Count Endpoint", () => {
|
|
|
39
55
|
});
|
|
40
56
|
|
|
41
57
|
it("GET /products/count should return a count object", async () => {
|
|
42
|
-
const
|
|
43
|
-
const app = generator.generateRoutes();
|
|
58
|
+
const app = createApp([collection], driver);
|
|
44
59
|
|
|
45
60
|
const res = await app.request("/products/count");
|
|
46
61
|
expect(res.status).toBe(200);
|
|
@@ -50,8 +65,7 @@ describe("RestApiGenerator - Count Endpoint", () => {
|
|
|
50
65
|
});
|
|
51
66
|
|
|
52
67
|
it("should pass filters to countEntities driver", async () => {
|
|
53
|
-
const
|
|
54
|
-
const app = generator.generateRoutes();
|
|
68
|
+
const app = createApp([collection], driver);
|
|
55
69
|
|
|
56
70
|
const res = await app.request("/products/count?status=eq.active");
|
|
57
71
|
expect(res.status).toBe(200);
|
|
@@ -67,8 +81,7 @@ describe("RestApiGenerator - Count Endpoint", () => {
|
|
|
67
81
|
});
|
|
68
82
|
|
|
69
83
|
it("should pass searchString to countEntities driver", async () => {
|
|
70
|
-
const
|
|
71
|
-
const app = generator.generateRoutes();
|
|
84
|
+
const app = createApp([collection], driver);
|
|
72
85
|
|
|
73
86
|
const res = await app.request("/products/count?searchString=widget");
|
|
74
87
|
expect(res.status).toBe(200);
|
|
@@ -83,8 +96,7 @@ describe("RestApiGenerator - Count Endpoint", () => {
|
|
|
83
96
|
|
|
84
97
|
it("should return 0 when countEntities is not available on driver", async () => {
|
|
85
98
|
const driverWithoutCount = createMockDriver({ countEntities: undefined });
|
|
86
|
-
const
|
|
87
|
-
const app = generator.generateRoutes();
|
|
99
|
+
const app = createApp([collection], driverWithoutCount);
|
|
88
100
|
|
|
89
101
|
const res = await app.request("/products/count");
|
|
90
102
|
expect(res.status).toBe(200);
|
|
@@ -100,8 +112,7 @@ describe("RestApiGenerator - Count Endpoint", () => {
|
|
|
100
112
|
countEntities: jest.fn<NonNullable<DataDriver["countEntities"]>>().mockResolvedValue(99),
|
|
101
113
|
fetchEntity: fetchEntity as unknown as DataDriver["fetchEntity"]
|
|
102
114
|
});
|
|
103
|
-
const
|
|
104
|
-
const app = generator.generateRoutes();
|
|
115
|
+
const app = createApp([collection], driverCustom);
|
|
105
116
|
|
|
106
117
|
const res = await app.request("/products/count");
|
|
107
118
|
expect(res.status).toBe(200);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import { DataDriver, Entity, EntityCollection, FetchCollectionProps, DataHooks, BackendHookContext, RestFetchService } from "@rebasepro/types";
|
|
2
|
+
import { AuthAdapter, DataDriver, Entity, EntityCollection, FetchCollectionProps, DataHooks, BackendHookContext, RestFetchService } from "@rebasepro/types";
|
|
3
3
|
import { QueryOptions, HonoEnv } from "../types";
|
|
4
|
-
import { ApiError } from "../errors";
|
|
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
|
/**
|
|
@@ -16,11 +17,18 @@ export class RestApiGenerator {
|
|
|
16
17
|
private router: Hono<HonoEnv>;
|
|
17
18
|
private driver: DataDriver;
|
|
18
19
|
private dataHooks?: DataHooks;
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
private authAdapter?: AuthAdapter;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
collections: EntityCollection[],
|
|
24
|
+
driver: DataDriver,
|
|
25
|
+
dataHooks?: DataHooks,
|
|
26
|
+
authAdapter?: AuthAdapter
|
|
27
|
+
) {
|
|
21
28
|
this.collections = collections;
|
|
22
29
|
this.driver = driver;
|
|
23
30
|
this.dataHooks = dataHooks;
|
|
31
|
+
this.authAdapter = authAdapter;
|
|
24
32
|
this.router = new Hono<HonoEnv>();
|
|
25
33
|
}
|
|
26
34
|
|
|
@@ -28,7 +36,8 @@ export class RestApiGenerator {
|
|
|
28
36
|
private buildHookContext(c: { get: (key: string) => unknown }, method: BackendHookContext["method"]): BackendHookContext {
|
|
29
37
|
const user = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
30
38
|
return {
|
|
31
|
-
requestUser: user ? { userId: user.userId,
|
|
39
|
+
requestUser: user ? { userId: user.userId,
|
|
40
|
+
roles: user.roles ?? [] } : undefined,
|
|
32
41
|
method
|
|
33
42
|
};
|
|
34
43
|
}
|
|
@@ -56,7 +65,7 @@ export class RestApiGenerator {
|
|
|
56
65
|
*/
|
|
57
66
|
private enforceApiKeyPermission(
|
|
58
67
|
c: { get: (key: string) => unknown; req: { method: string } },
|
|
59
|
-
collectionSlug: string
|
|
68
|
+
collectionSlug: string
|
|
60
69
|
): void {
|
|
61
70
|
const apiKey = c.get("apiKey") as ApiKeyMasked | undefined;
|
|
62
71
|
if (!apiKey) return; // Not an API key request — skip
|
|
@@ -65,11 +74,21 @@ export class RestApiGenerator {
|
|
|
65
74
|
if (!isOperationAllowed(apiKey.permissions, collectionSlug, operation)) {
|
|
66
75
|
throw ApiError.forbidden(
|
|
67
76
|
`API key does not have "${operation}" permission for collection "${collectionSlug}"`,
|
|
68
|
-
"API_KEY_FORBIDDEN"
|
|
77
|
+
"API_KEY_FORBIDDEN"
|
|
69
78
|
);
|
|
70
79
|
}
|
|
71
80
|
}
|
|
72
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Get the request-scoped driver. Throws if none is set — never falls
|
|
84
|
+
* back to the unscoped `this.driver` to avoid bypassing RLS/auth.
|
|
85
|
+
*/
|
|
86
|
+
private getScopedDriver(c: { get: (key: string) => unknown }): DataDriver {
|
|
87
|
+
const driver = c.get("driver") as DataDriver | undefined;
|
|
88
|
+
if (!driver) throw ApiError.internal("Scoped driver not available");
|
|
89
|
+
return driver;
|
|
90
|
+
}
|
|
91
|
+
|
|
73
92
|
/**
|
|
74
93
|
* Get the typed RestFetchService from a driver if it exposes one (for include support).
|
|
75
94
|
*/
|
|
@@ -90,7 +109,7 @@ export class RestApiGenerator {
|
|
|
90
109
|
const queryDict = c.req.queries();
|
|
91
110
|
const queryOptions = parseQueryOptions(queryDict);
|
|
92
111
|
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : undefined;
|
|
93
|
-
const driver =
|
|
112
|
+
const driver = this.getScopedDriver(c);
|
|
94
113
|
|
|
95
114
|
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
|
|
96
115
|
return c.json({ count: total });
|
|
@@ -103,7 +122,7 @@ export class RestApiGenerator {
|
|
|
103
122
|
const queryOptions = parseQueryOptions(queryDict);
|
|
104
123
|
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : undefined;
|
|
105
124
|
|
|
106
|
-
const driver =
|
|
125
|
+
const driver = this.getScopedDriver(c);
|
|
107
126
|
const fetchService = this.getFetchService(driver);
|
|
108
127
|
const hookCtx = this.buildHookContext(c, "GET");
|
|
109
128
|
|
|
@@ -163,7 +182,7 @@ export class RestApiGenerator {
|
|
|
163
182
|
const id = c.req.param("id");
|
|
164
183
|
const queryDict = c.req.queries();
|
|
165
184
|
const queryOptions = parseQueryOptions(queryDict);
|
|
166
|
-
const driver =
|
|
185
|
+
const driver = this.getScopedDriver(c);
|
|
167
186
|
const fetchService = this.getFetchService(driver);
|
|
168
187
|
const hookCtx = this.buildHookContext(c, "GET");
|
|
169
188
|
|
|
@@ -207,7 +226,7 @@ export class RestApiGenerator {
|
|
|
207
226
|
this.router.post(basePath, async (c) => {
|
|
208
227
|
try {
|
|
209
228
|
this.enforceApiKeyPermission(c, collection.slug);
|
|
210
|
-
const driver =
|
|
229
|
+
const driver = this.getScopedDriver(c);
|
|
211
230
|
const path = collection.slug;
|
|
212
231
|
const hookCtx = this.buildHookContext(c, "POST");
|
|
213
232
|
|
|
@@ -217,6 +236,46 @@ export class RestApiGenerator {
|
|
|
217
236
|
body = await this.dataHooks.beforeSave(path, body, undefined, hookCtx);
|
|
218
237
|
}
|
|
219
238
|
|
|
239
|
+
const isAuth = collection.auth;
|
|
240
|
+
const isAuthCollection = isAuth === true || (isAuth && typeof isAuth === "object" && isAuth.enabled === true);
|
|
241
|
+
|
|
242
|
+
if (isAuthCollection && this.authAdapter?.prepareUserCreation) {
|
|
243
|
+
const collectionAuthConfig = typeof isAuth === "object" ? isAuth : undefined;
|
|
244
|
+
const prepared = await this.authAdapter.prepareUserCreation(body, collectionAuthConfig);
|
|
245
|
+
|
|
246
|
+
const entity = await driver.saveEntity({
|
|
247
|
+
path,
|
|
248
|
+
values: prepared.values,
|
|
249
|
+
collection: resolvedCollection,
|
|
250
|
+
status: "new"
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const result = prepared.hookHandledEmail
|
|
254
|
+
? { temporaryPassword: prepared.clearPassword,
|
|
255
|
+
invitationSent: prepared.invitationSent }
|
|
256
|
+
: this.authAdapter.finalizeUserCreation
|
|
257
|
+
? await this.authAdapter.finalizeUserCreation(
|
|
258
|
+
{ id: entity.id as string,
|
|
259
|
+
values: entity.values as Record<string, unknown> },
|
|
260
|
+
prepared.clearPassword
|
|
261
|
+
)
|
|
262
|
+
: { invitationSent: false };
|
|
263
|
+
|
|
264
|
+
const response = this.formatResponse(entity) as Record<string, unknown>;
|
|
265
|
+
|
|
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
|
+
}
|
|
271
|
+
|
|
272
|
+
return c.json({
|
|
273
|
+
...response,
|
|
274
|
+
invitationSent: result.invitationSent,
|
|
275
|
+
...(result.temporaryPassword ? { temporaryPassword: result.temporaryPassword } : {})
|
|
276
|
+
}, 201);
|
|
277
|
+
}
|
|
278
|
+
|
|
220
279
|
const entity = await driver.saveEntity({
|
|
221
280
|
path,
|
|
222
281
|
values: body,
|
|
@@ -228,15 +287,16 @@ export class RestApiGenerator {
|
|
|
228
287
|
|
|
229
288
|
if (this.dataHooks?.afterSave) {
|
|
230
289
|
Promise.resolve(this.dataHooks.afterSave(path, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
231
|
-
|
|
290
|
+
logger.error("[BackendHooks] data.afterSave error", { error: err instanceof Error ? err.message : err });
|
|
232
291
|
});
|
|
233
292
|
}
|
|
234
293
|
|
|
235
294
|
return c.json(response, 201);
|
|
236
295
|
} catch (error) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
296
|
+
if (isRebaseApiError(error) && !error.code) {
|
|
297
|
+
error.code = "BAD_REQUEST";
|
|
298
|
+
}
|
|
299
|
+
throw error;
|
|
240
300
|
}
|
|
241
301
|
});
|
|
242
302
|
|
|
@@ -245,7 +305,7 @@ export class RestApiGenerator {
|
|
|
245
305
|
try {
|
|
246
306
|
this.enforceApiKeyPermission(c, collection.slug);
|
|
247
307
|
const id = c.req.param("id");
|
|
248
|
-
const driver =
|
|
308
|
+
const driver = this.getScopedDriver(c);
|
|
249
309
|
const hookCtx = this.buildHookContext(c, "PUT");
|
|
250
310
|
|
|
251
311
|
const existingEntity = await driver.fetchEntity({
|
|
@@ -276,15 +336,16 @@ export class RestApiGenerator {
|
|
|
276
336
|
|
|
277
337
|
if (this.dataHooks?.afterSave) {
|
|
278
338
|
Promise.resolve(this.dataHooks.afterSave(collection.slug, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
279
|
-
|
|
339
|
+
logger.error("[BackendHooks] data.afterSave error", { error: err instanceof Error ? err.message : err });
|
|
280
340
|
});
|
|
281
341
|
}
|
|
282
342
|
|
|
283
343
|
return c.json(response);
|
|
284
344
|
} catch (error) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
345
|
+
if (isRebaseApiError(error) && !error.code) {
|
|
346
|
+
error.code = "BAD_REQUEST";
|
|
347
|
+
}
|
|
348
|
+
throw error;
|
|
288
349
|
}
|
|
289
350
|
});
|
|
290
351
|
|
|
@@ -292,7 +353,7 @@ export class RestApiGenerator {
|
|
|
292
353
|
this.router.delete(`${basePath}/:id`, async (c) => {
|
|
293
354
|
this.enforceApiKeyPermission(c, collection.slug);
|
|
294
355
|
const id = c.req.param("id");
|
|
295
|
-
const driver =
|
|
356
|
+
const driver = this.getScopedDriver(c);
|
|
296
357
|
const hookCtx = this.buildHookContext(c, "DELETE");
|
|
297
358
|
|
|
298
359
|
const existingEntity = await driver.fetchEntity({
|
|
@@ -316,7 +377,7 @@ export class RestApiGenerator {
|
|
|
316
377
|
|
|
317
378
|
if (this.dataHooks?.afterDelete) {
|
|
318
379
|
Promise.resolve(this.dataHooks.afterDelete(collection.slug, String(id), hookCtx)).catch(err => {
|
|
319
|
-
|
|
380
|
+
logger.error("[BackendHooks] data.afterDelete error", { error: err instanceof Error ? err.message : err });
|
|
320
381
|
});
|
|
321
382
|
}
|
|
322
383
|
|
|
@@ -382,22 +443,24 @@ entityId };
|
|
|
382
443
|
const parsed = parseSubPath(rawPath);
|
|
383
444
|
if (!parsed) return next();
|
|
384
445
|
|
|
385
|
-
const driver =
|
|
446
|
+
const driver = this.getScopedDriver(c);
|
|
386
447
|
|
|
387
448
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
388
449
|
|
|
450
|
+
const hookCtx = this.buildHookContext(c, "GET");
|
|
451
|
+
|
|
389
452
|
if (parsed.entityId === "count") {
|
|
390
453
|
// GET /parent/:parentId/child/count — count child entities
|
|
391
454
|
const queryDict = c.req.queries();
|
|
392
455
|
const queryOptions = parseQueryOptions(queryDict);
|
|
393
456
|
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : undefined;
|
|
394
|
-
|
|
457
|
+
|
|
395
458
|
const total = driver.countEntities ? await driver.countEntities({
|
|
396
459
|
path: parsed.collectionPath,
|
|
397
460
|
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
398
461
|
searchString
|
|
399
462
|
}) : 0;
|
|
400
|
-
|
|
463
|
+
|
|
401
464
|
return c.json({ count: total });
|
|
402
465
|
} else if (parsed.entityId) {
|
|
403
466
|
// GET /parent/:parentId/child/:id — single entity
|
|
@@ -406,7 +469,12 @@ entityId };
|
|
|
406
469
|
entityId: parsed.entityId
|
|
407
470
|
});
|
|
408
471
|
if (!entity) throw ApiError.notFound("Entity not found");
|
|
409
|
-
|
|
472
|
+
|
|
473
|
+
const flatResult = this.flattenEntity(entity);
|
|
474
|
+
const hookResult = await this.applyAfterRead(c.req.param("parent"), flatResult, hookCtx);
|
|
475
|
+
if (!hookResult) throw ApiError.notFound("Entity not found");
|
|
476
|
+
|
|
477
|
+
return c.json(hookResult);
|
|
410
478
|
} else {
|
|
411
479
|
// GET /parent/:parentId/child — list entities
|
|
412
480
|
const queryDict = c.req.queries();
|
|
@@ -420,13 +488,23 @@ entityId };
|
|
|
420
488
|
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
421
489
|
searchString
|
|
422
490
|
});
|
|
491
|
+
|
|
492
|
+
let flatEntities = entities.map(e => this.flattenEntity(e));
|
|
493
|
+
flatEntities = await this.applyAfterReadBatch(c.req.param("parent"), flatEntities, hookCtx);
|
|
494
|
+
|
|
495
|
+
const total = driver.countEntities ? await driver.countEntities({
|
|
496
|
+
path: parsed.collectionPath,
|
|
497
|
+
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
498
|
+
searchString
|
|
499
|
+
}) : flatEntities.length;
|
|
500
|
+
|
|
423
501
|
return c.json({
|
|
424
|
-
data:
|
|
502
|
+
data: flatEntities,
|
|
425
503
|
meta: {
|
|
426
|
-
total
|
|
504
|
+
total,
|
|
427
505
|
limit: queryOptions.limit,
|
|
428
506
|
offset: queryOptions.offset,
|
|
429
|
-
hasMore:
|
|
507
|
+
hasMore: (queryOptions.offset || 0) + flatEntities.length < total
|
|
430
508
|
}
|
|
431
509
|
});
|
|
432
510
|
}
|
|
@@ -440,10 +518,15 @@ entityId };
|
|
|
440
518
|
const parsed = parseSubPath(rawPath);
|
|
441
519
|
if (!parsed || parsed.entityId) return next();
|
|
442
520
|
|
|
443
|
-
const driver =
|
|
521
|
+
const driver = this.getScopedDriver(c);
|
|
522
|
+
const hookCtx = this.buildHookContext(c, "POST");
|
|
444
523
|
|
|
445
524
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
446
|
-
|
|
525
|
+
let body = await c.req.json().catch(() => ({}));
|
|
526
|
+
|
|
527
|
+
if (this.dataHooks?.beforeSave) {
|
|
528
|
+
body = await this.dataHooks.beforeSave(parsed.collectionPath, body, undefined, hookCtx);
|
|
529
|
+
}
|
|
447
530
|
|
|
448
531
|
const entity = await driver.saveEntity({
|
|
449
532
|
path: parsed.collectionPath,
|
|
@@ -451,7 +534,15 @@ entityId };
|
|
|
451
534
|
status: "new"
|
|
452
535
|
});
|
|
453
536
|
|
|
454
|
-
|
|
537
|
+
const response = this.formatResponse(entity);
|
|
538
|
+
|
|
539
|
+
if (this.dataHooks?.afterSave) {
|
|
540
|
+
Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
541
|
+
logger.error("[BackendHooks] data.afterSave error", { error: err instanceof Error ? err.message : err });
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return c.json(response, 201);
|
|
455
546
|
});
|
|
456
547
|
|
|
457
548
|
// PUT /<subcollection-path>/:id — update entity
|
|
@@ -462,11 +553,16 @@ entityId };
|
|
|
462
553
|
const parsed = parseSubPath(rawPath);
|
|
463
554
|
if (!parsed || !parsed.entityId) return next();
|
|
464
555
|
|
|
465
|
-
const driver =
|
|
556
|
+
const driver = this.getScopedDriver(c);
|
|
557
|
+
const hookCtx = this.buildHookContext(c, "PUT");
|
|
466
558
|
|
|
467
559
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
468
560
|
|
|
469
|
-
|
|
561
|
+
let body = await c.req.json().catch(() => ({}));
|
|
562
|
+
|
|
563
|
+
if (this.dataHooks?.beforeSave) {
|
|
564
|
+
body = await this.dataHooks.beforeSave(parsed.collectionPath, body, parsed.entityId, hookCtx);
|
|
565
|
+
}
|
|
470
566
|
|
|
471
567
|
const entity = await driver.saveEntity({
|
|
472
568
|
path: parsed.collectionPath,
|
|
@@ -475,7 +571,15 @@ entityId };
|
|
|
475
571
|
status: "existing"
|
|
476
572
|
});
|
|
477
573
|
|
|
478
|
-
|
|
574
|
+
const response = this.formatResponse(entity);
|
|
575
|
+
|
|
576
|
+
if (this.dataHooks?.afterSave) {
|
|
577
|
+
Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
578
|
+
logger.error("[BackendHooks] data.afterSave error", { error: err instanceof Error ? err.message : err });
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return c.json(response);
|
|
479
583
|
});
|
|
480
584
|
|
|
481
585
|
// DELETE /<subcollection-path>/:id — delete entity
|
|
@@ -486,7 +590,8 @@ entityId };
|
|
|
486
590
|
const parsed = parseSubPath(rawPath);
|
|
487
591
|
if (!parsed || !parsed.entityId) return next();
|
|
488
592
|
|
|
489
|
-
const driver =
|
|
593
|
+
const driver = this.getScopedDriver(c);
|
|
594
|
+
const hookCtx = this.buildHookContext(c, "DELETE");
|
|
490
595
|
|
|
491
596
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
492
597
|
|
|
@@ -497,8 +602,18 @@ entityId };
|
|
|
497
602
|
|
|
498
603
|
if (!existingEntity) throw ApiError.notFound("Entity not found");
|
|
499
604
|
|
|
605
|
+
if (this.dataHooks?.beforeDelete) {
|
|
606
|
+
await this.dataHooks.beforeDelete(parsed.collectionPath, parsed.entityId, hookCtx);
|
|
607
|
+
}
|
|
608
|
+
|
|
500
609
|
await driver.deleteEntity({ entity: existingEntity });
|
|
501
610
|
|
|
611
|
+
if (this.dataHooks?.afterDelete) {
|
|
612
|
+
Promise.resolve(this.dataHooks.afterDelete(parsed.collectionPath, parsed.entityId, hookCtx)).catch(err => {
|
|
613
|
+
logger.error("[BackendHooks] data.afterDelete error", { error: err instanceof Error ? err.message : err });
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
502
617
|
return new Response(null, { status: 204 });
|
|
503
618
|
});
|
|
504
619
|
}
|
|
@@ -31,17 +31,21 @@ export function parseLogicalString(str: string): FilterCondition | LogicalCondit
|
|
|
31
31
|
str = str.trim();
|
|
32
32
|
if (str.startsWith("or(") && str.endsWith(")")) {
|
|
33
33
|
const inner = str.slice(3, -1);
|
|
34
|
-
return { type: "or",
|
|
34
|
+
return { type: "or",
|
|
35
|
+
conditions: parseLogicalList(inner) };
|
|
35
36
|
}
|
|
36
37
|
if (str.startsWith("and(") && str.endsWith(")")) {
|
|
37
38
|
const inner = str.slice(4, -1);
|
|
38
|
-
return { type: "and",
|
|
39
|
+
return { type: "and",
|
|
40
|
+
conditions: parseLogicalList(inner) };
|
|
39
41
|
}
|
|
40
|
-
|
|
42
|
+
|
|
41
43
|
// It's a leaf condition: field.op.val
|
|
42
44
|
const firstDot = str.indexOf(".");
|
|
43
45
|
if (firstDot === -1) {
|
|
44
|
-
return { column: str,
|
|
46
|
+
return { column: str,
|
|
47
|
+
operator: "==",
|
|
48
|
+
value: true };
|
|
45
49
|
}
|
|
46
50
|
const field = str.substring(0, firstDot);
|
|
47
51
|
const rest = str.substring(firstDot + 1);
|
|
@@ -55,7 +59,7 @@ export function parseLogicalString(str: string): FilterCondition | LogicalCondit
|
|
|
55
59
|
op = "eq";
|
|
56
60
|
valStr = rest;
|
|
57
61
|
}
|
|
58
|
-
|
|
62
|
+
|
|
59
63
|
const rebaseOp = (mapOperator(op) || "==") as FilterCondition["operator"];
|
|
60
64
|
let parsedVal: unknown = valStr;
|
|
61
65
|
if (valStr === "true") parsedVal = true;
|
|
@@ -73,20 +77,22 @@ export function parseLogicalString(str: string): FilterCondition | LogicalCondit
|
|
|
73
77
|
return trimmed;
|
|
74
78
|
});
|
|
75
79
|
}
|
|
76
|
-
|
|
77
|
-
return { column: field,
|
|
80
|
+
|
|
81
|
+
return { column: field,
|
|
82
|
+
operator: rebaseOp,
|
|
83
|
+
value: parsedVal };
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
function parseLogicalList(str: string): (FilterCondition | LogicalCondition)[] {
|
|
81
87
|
const list: (FilterCondition | LogicalCondition)[] = [];
|
|
82
88
|
let depth = 0;
|
|
83
89
|
let current = "";
|
|
84
|
-
|
|
90
|
+
|
|
85
91
|
for (let i = 0; i < str.length; i++) {
|
|
86
92
|
const char = str[i];
|
|
87
93
|
if (char === "(") depth++;
|
|
88
94
|
if (char === ")") depth--;
|
|
89
|
-
|
|
95
|
+
|
|
90
96
|
if (char === "," && depth === 0) {
|
|
91
97
|
list.push(parseLogicalString(current));
|
|
92
98
|
current = "";
|
|
@@ -109,10 +115,10 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
|
|
|
109
115
|
// Pagination
|
|
110
116
|
const limitVal = getLastValue(query.limit);
|
|
111
117
|
if (limitVal) options.limit = parseInt(String(limitVal));
|
|
112
|
-
|
|
118
|
+
|
|
113
119
|
const offsetVal = getLastValue(query.offset);
|
|
114
120
|
if (offsetVal) options.offset = parseInt(String(offsetVal));
|
|
115
|
-
|
|
121
|
+
|
|
116
122
|
const pageVal = getLastValue(query.page);
|
|
117
123
|
if (pageVal) {
|
|
118
124
|
const page = parseInt(String(pageVal));
|
|
@@ -275,7 +281,7 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
|
|
|
275
281
|
const vectorSearch: VectorSearchParams = {
|
|
276
282
|
property: String(vectorSearchVal),
|
|
277
283
|
vector: queryVector,
|
|
278
|
-
distance: distanceParam
|
|
284
|
+
distance: distanceParam
|
|
279
285
|
};
|
|
280
286
|
|
|
281
287
|
const thresholdVal = getLastValue(query.vector_threshold);
|