@rebasepro/server-core 0.7.0 → 0.9.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 +2 -2
- package/dist/api/ast-schema-editor.d.ts +21 -0
- package/dist/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
- package/dist/api/errors.d.ts +53 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/logs-routes.d.ts +37 -0
- package/dist/api/openapi-generator.d.ts +16 -0
- package/dist/api/rest/api-generator.d.ts +68 -0
- package/{src/api/rest/index.ts → dist/api/rest/index.d.ts} +0 -1
- package/dist/api/rest/query-parser.d.ts +6 -0
- package/dist/api/schema-editor-routes.d.ts +3 -0
- package/dist/api/types.d.ts +56 -0
- package/dist/auth/adapter-middleware.d.ts +43 -0
- package/dist/auth/admin-roles-route.d.ts +18 -0
- package/dist/auth/admin-user-ops.d.ts +79 -0
- package/dist/auth/admin-users-route.d.ts +28 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +39 -0
- package/{src/auth/api-keys/api-key-permission-guard.ts → dist/auth/api-keys/api-key-permission-guard.d.ts} +2 -34
- package/dist/auth/api-keys/api-key-routes.d.ts +20 -0
- package/dist/auth/api-keys/api-key-store.d.ts +35 -0
- package/{src/auth/api-keys/api-key-types.ts → dist/auth/api-keys/api-key-types.d.ts} +0 -6
- package/{src/auth/api-keys/index.ts → dist/auth/api-keys/index.d.ts} +2 -22
- package/dist/auth/apple-oauth.d.ts +30 -0
- package/{src/auth/auth-hooks.ts → dist/auth/auth-hooks.d.ts} +15 -73
- package/dist/auth/bitbucket-oauth.d.ts +11 -0
- package/dist/auth/builtin-auth-adapter.d.ts +55 -0
- package/dist/auth/cookie-utils.d.ts +32 -0
- package/{src/auth/crypto-utils.ts → dist/auth/crypto-utils.d.ts} +1 -16
- package/dist/auth/custom-auth-adapter.d.ts +39 -0
- package/dist/auth/discord-oauth.d.ts +14 -0
- package/dist/auth/facebook-oauth.d.ts +14 -0
- package/dist/auth/github-oauth.d.ts +15 -0
- package/dist/auth/gitlab-oauth.d.ts +13 -0
- package/dist/auth/google-oauth.d.ts +47 -0
- package/{src/auth/index.ts → dist/auth/index.d.ts} +4 -22
- package/{src/auth/interfaces.ts → dist/auth/interfaces.d.ts} +20 -89
- package/dist/auth/jwt.d.ts +67 -0
- package/dist/auth/linkedin-oauth.d.ts +18 -0
- package/dist/auth/magic-link-routes.d.ts +32 -0
- package/dist/auth/mfa-crypto.d.ts +23 -0
- package/dist/auth/mfa-routes.d.ts +7 -0
- package/dist/auth/mfa.d.ts +49 -0
- package/dist/auth/microsoft-oauth.d.ts +16 -0
- package/dist/auth/middleware.d.ts +158 -0
- package/dist/auth/password.d.ts +22 -0
- package/dist/auth/rate-limiter.d.ts +50 -0
- package/dist/auth/reset-password-admin.d.ts +29 -0
- package/dist/auth/rls-scope.d.ts +31 -0
- package/dist/auth/routes.d.ts +66 -0
- package/dist/auth/session-routes.d.ts +29 -0
- package/dist/auth/slack-oauth.d.ts +12 -0
- package/dist/auth/spotify-oauth.d.ts +12 -0
- package/dist/auth/twitter-oauth.d.ts +18 -0
- package/dist/collections/BackendCollectionRegistry.d.ts +13 -0
- package/dist/collections/loader.d.ts +5 -0
- package/dist/cron/cron-loader.d.ts +18 -0
- package/dist/cron/cron-routes.d.ts +14 -0
- package/dist/cron/cron-scheduler.d.ts +106 -0
- package/dist/cron/cron-store.d.ts +32 -0
- package/dist/cron/define-cron.d.ts +28 -0
- package/{src/cron/index.ts → dist/cron/index.d.ts} +1 -0
- package/dist/db/interfaces.d.ts +18 -0
- package/dist/dynamic-import-Dvh-K5fl.js +22 -0
- package/dist/dynamic-import-Dvh-K5fl.js.map +1 -0
- package/dist/email/index.d.ts +6 -0
- package/dist/email/smtp-email-service.d.ts +30 -0
- package/dist/email/templates.d.ts +50 -0
- package/{src/email/types.ts → dist/email/types.d.ts} +40 -36
- package/dist/env.d.ts +102 -0
- package/dist/functions/define-function.d.ts +55 -0
- package/dist/functions/function-loader.d.ts +18 -0
- package/dist/functions/function-routes.d.ts +10 -0
- package/{src/functions/index.ts → dist/functions/index.d.ts} +2 -0
- package/dist/history/history-routes.d.ts +23 -0
- package/{src/history/index.ts → dist/history/index.d.ts} +0 -1
- package/dist/index.d.ts +35 -0
- package/dist/index.es.js +18184 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +48629 -0
- package/dist/index.umd.js.map +1 -0
- 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 +65 -0
- package/dist/init/storage.d.ts +5 -0
- package/dist/init.d.ts +344 -0
- package/dist/jwt-BwIn8xmk.js +4200 -0
- package/dist/jwt-BwIn8xmk.js.map +1 -0
- package/dist/logger-BYU66ENZ.js +94 -0
- package/dist/logger-BYU66ENZ.js.map +1 -0
- package/dist/ms-DnYXB-Wd.js +162 -0
- package/dist/ms-DnYXB-Wd.js.map +1 -0
- package/dist/openapi-generator-Z9oYWLf_.js +586 -0
- package/dist/openapi-generator-Z9oYWLf_.js.map +1 -0
- package/dist/schema-editor-routes-D3ef8zu1.js +219 -0
- package/dist/schema-editor-routes-D3ef8zu1.js.map +1 -0
- package/dist/serve-spa.d.ts +36 -0
- package/dist/services/driver-registry.d.ts +78 -0
- package/dist/services/routed-realtime-service.d.ts +43 -0
- package/dist/services/webhook-service.d.ts +29 -0
- package/dist/singleton.d.ts +51 -0
- package/dist/src-B4OLmNVa.js +437 -0
- package/dist/src-B4OLmNVa.js.map +1 -0
- package/dist/src-B5WkP1Rv.js +24778 -0
- package/dist/src-B5WkP1Rv.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +43 -0
- package/dist/storage/LocalStorageController.d.ts +46 -0
- package/dist/storage/S3StorageController.d.ts +40 -0
- package/dist/storage/image-transform.d.ts +50 -0
- package/{src/storage/index.ts → dist/storage/index.d.ts} +6 -21
- package/dist/storage/routes.d.ts +70 -0
- package/dist/storage/storage-registry.d.ts +78 -0
- package/dist/storage/tus-handler.d.ts +53 -0
- package/{src/storage/types.ts → dist/storage/types.d.ts} +32 -38
- package/dist/types/index.d.ts +11 -0
- package/dist/utils/dev-port.d.ts +45 -0
- package/dist/utils/dynamic-import.d.ts +25 -0
- package/dist/utils/logger.d.ts +31 -0
- package/dist/utils/logging.d.ts +9 -0
- package/dist/utils/request-id.d.ts +4 -0
- package/dist/utils/request-logger.d.ts +19 -0
- package/{src/utils/sql.ts → dist/utils/sql.d.ts} +5 -16
- package/package.json +49 -22
- package/coverage/clover.xml +0 -3739
- package/coverage/coverage-final.json +0 -31
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -266
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +0 -952
- package/coverage/lcov-report/src/api/errors.ts.html +0 -472
- package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +0 -1069
- package/coverage/lcov-report/src/api/graphql/index.html +0 -116
- package/coverage/lcov-report/src/api/index.html +0 -176
- package/coverage/lcov-report/src/api/openapi-generator.ts.html +0 -565
- package/coverage/lcov-report/src/api/rest/api-generator.ts.html +0 -994
- package/coverage/lcov-report/src/api/rest/index.html +0 -131
- package/coverage/lcov-report/src/api/rest/query-parser.ts.html +0 -550
- package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +0 -202
- package/coverage/lcov-report/src/api/server.ts.html +0 -823
- package/coverage/lcov-report/src/auth/admin-routes.ts.html +0 -973
- package/coverage/lcov-report/src/auth/index.html +0 -176
- package/coverage/lcov-report/src/auth/jwt.ts.html +0 -574
- package/coverage/lcov-report/src/auth/middleware.ts.html +0 -745
- package/coverage/lcov-report/src/auth/password.ts.html +0 -310
- package/coverage/lcov-report/src/auth/services.ts.html +0 -2074
- package/coverage/lcov-report/src/collections/index.html +0 -116
- package/coverage/lcov-report/src/collections/loader.ts.html +0 -232
- package/coverage/lcov-report/src/db/auth-schema.ts.html +0 -523
- package/coverage/lcov-report/src/db/data-transformer.ts.html +0 -1753
- package/coverage/lcov-report/src/db/entityService.ts.html +0 -700
- package/coverage/lcov-report/src/db/index.html +0 -146
- package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +0 -4048
- package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +0 -883
- package/coverage/lcov-report/src/db/services/RelationService.ts.html +0 -3121
- package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +0 -442
- package/coverage/lcov-report/src/db/services/index.html +0 -176
- package/coverage/lcov-report/src/db/services/index.ts.html +0 -124
- package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +0 -1960
- package/coverage/lcov-report/src/index.html +0 -116
- package/coverage/lcov-report/src/services/driver-registry.ts.html +0 -631
- package/coverage/lcov-report/src/services/index.html +0 -131
- package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +0 -3025
- package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +0 -1189
- package/coverage/lcov-report/src/storage/S3StorageController.ts.html +0 -970
- package/coverage/lcov-report/src/storage/index.html +0 -161
- package/coverage/lcov-report/src/storage/storage-registry.ts.html +0 -646
- package/coverage/lcov-report/src/storage/types.ts.html +0 -451
- package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +0 -3082
- package/coverage/lcov-report/src/utils/index.html +0 -116
- package/coverage/lcov.info +0 -7179
- package/jest.config.cjs +0 -19
- package/src/api/ast-schema-editor.ts +0 -289
- package/src/api/collections_for_test/callbacks_test_collection.ts +0 -61
- package/src/api/errors.ts +0 -278
- package/src/api/graphql/graphql-schema-generator.ts +0 -383
- package/src/api/graphql/index.ts +0 -2
- package/src/api/index.ts +0 -11
- package/src/api/logs-routes.ts +0 -132
- package/src/api/openapi-generator.ts +0 -727
- package/src/api/rest/api-generator-count.test.ts +0 -126
- package/src/api/rest/api-generator.ts +0 -747
- package/src/api/rest/query-parser.ts +0 -300
- package/src/api/schema-editor-routes.ts +0 -41
- package/src/api/server.ts +0 -271
- package/src/api/types.ts +0 -104
- package/src/auth/adapter-middleware.ts +0 -114
- package/src/auth/admin-roles-route.ts +0 -36
- package/src/auth/admin-user-ops.ts +0 -236
- package/src/auth/admin-users-route.ts +0 -302
- package/src/auth/api-keys/api-key-middleware.ts +0 -133
- package/src/auth/api-keys/api-key-routes.ts +0 -193
- package/src/auth/api-keys/api-key-store.ts +0 -340
- package/src/auth/apple-oauth.ts +0 -122
- package/src/auth/bitbucket-oauth.ts +0 -84
- package/src/auth/builtin-auth-adapter.ts +0 -380
- package/src/auth/custom-auth-adapter.ts +0 -86
- package/src/auth/discord-oauth.ts +0 -85
- package/src/auth/facebook-oauth.ts +0 -74
- package/src/auth/github-oauth.ts +0 -112
- package/src/auth/gitlab-oauth.ts +0 -72
- package/src/auth/google-oauth.ts +0 -223
- package/src/auth/jwt.ts +0 -208
- package/src/auth/linkedin-oauth.ts +0 -83
- package/src/auth/magic-link-routes.ts +0 -167
- package/src/auth/mfa-crypto.ts +0 -91
- package/src/auth/mfa-routes.ts +0 -323
- package/src/auth/mfa.ts +0 -161
- package/src/auth/microsoft-oauth.ts +0 -90
- package/src/auth/middleware.ts +0 -369
- package/src/auth/password.ts +0 -77
- package/src/auth/rate-limiter.ts +0 -225
- package/src/auth/reset-password-admin.ts +0 -160
- package/src/auth/rls-scope.ts +0 -58
- package/src/auth/routes.ts +0 -802
- package/src/auth/session-routes.ts +0 -353
- package/src/auth/slack-oauth.ts +0 -73
- package/src/auth/spotify-oauth.ts +0 -69
- package/src/auth/twitter-oauth.ts +0 -128
- package/src/collections/BackendCollectionRegistry.ts +0 -20
- package/src/collections/loader.ts +0 -50
- package/src/cron/cron-loader.ts +0 -84
- package/src/cron/cron-routes.test.ts +0 -265
- package/src/cron/cron-routes.ts +0 -85
- package/src/cron/cron-scheduler.test.ts +0 -559
- package/src/cron/cron-scheduler.ts +0 -581
- package/src/cron/cron-store.ts +0 -166
- package/src/db/interfaces.ts +0 -60
- package/src/email/index.ts +0 -19
- package/src/email/smtp-email-service.ts +0 -123
- package/src/email/templates.ts +0 -470
- package/src/env.ts +0 -227
- package/src/functions/function-loader.ts +0 -116
- package/src/functions/function-routes.ts +0 -31
- package/src/history/history-routes.ts +0 -129
- package/src/index.ts +0 -70
- package/src/init/docs.ts +0 -47
- package/src/init/health.ts +0 -37
- package/src/init/middlewares.ts +0 -61
- package/src/init/shutdown.ts +0 -56
- package/src/init/storage.ts +0 -57
- package/src/init.ts +0 -1041
- package/src/serve-spa.ts +0 -89
- package/src/services/driver-registry.ts +0 -183
- package/src/services/routed-realtime-service.ts +0 -113
- package/src/services/webhook-service.ts +0 -155
- package/src/singleton.ts +0 -70
- package/src/storage/LocalStorageController.ts +0 -389
- package/src/storage/S3StorageController.ts +0 -300
- package/src/storage/image-transform.ts +0 -218
- package/src/storage/routes.ts +0 -368
- package/src/storage/storage-registry.ts +0 -188
- package/src/storage/tus-handler.ts +0 -315
- package/src/types/index.ts +0 -27
- package/src/utils/dev-port.ts +0 -196
- package/src/utils/logger.ts +0 -143
- package/src/utils/logging.ts +0 -40
- package/src/utils/request-id.ts +0 -40
- package/src/utils/request-logger.ts +0 -72
- package/test/api-generator.test.ts +0 -588
- package/test/api-key-permission-guard.test.ts +0 -144
- package/test/ast-schema-editor.test.ts +0 -89
- package/test/auth-middleware-hono.test.ts +0 -556
- package/test/auth-routes.test.ts +0 -1098
- package/test/backend-hooks-data.test.ts +0 -477
- package/test/custom-auth-adapter.test.ts +0 -189
- package/test/driver-registry.test.ts +0 -282
- package/test/email-templates.test.ts +0 -174
- package/test/env.test.ts +0 -128
- package/test/error-propagation.test.ts +0 -226
- package/test/errors-hono.test.ts +0 -133
- package/test/errors.test.ts +0 -155
- package/test/function-loader.test.ts +0 -127
- package/test/graphql-schema-generator.test.ts +0 -554
- package/test/jwt-security.test.ts +0 -182
- package/test/jwt.test.ts +0 -326
- package/test/mfa.test.ts +0 -197
- package/test/middleware.test.ts +0 -305
- package/test/multi-datasource-routing.test.ts +0 -113
- package/test/password.test.ts +0 -165
- package/test/query-parser.test.ts +0 -233
- package/test/rate-limiter.test.ts +0 -102
- package/test/reset-password-admin.test.ts +0 -113
- package/test/routed-realtime-service.test.ts +0 -86
- package/test/safe-compare.test.ts +0 -66
- package/test/singleton.test.ts +0 -59
- package/test/smtp-email-service.test.ts +0 -169
- package/test/storage-local.test.ts +0 -271
- package/test/storage-registry.test.ts +0 -282
- package/test/storage-routes.test.ts +0 -222
- package/test/storage-s3.test.ts +0 -304
- package/test/transform-auth-response.test.ts +0 -305
- package/test/webhook-service.test.ts +0 -260
- package/tsconfig.json +0 -54
- package/tsconfig.prod.json +0 -23
- package/vite.config.ts +0 -86
|
@@ -1,747 +0,0 @@
|
|
|
1
|
-
import { Hono } from "hono";
|
|
2
|
-
import { AuthAdapter, DataDriver, Entity, EntityCollection, FetchCollectionProps, DataHooks, BackendHookContext, RestFetchService } from "@rebasepro/types";
|
|
3
|
-
import { QueryOptions, HonoEnv } from "../types";
|
|
4
|
-
import { ApiError, isRebaseApiError } from "../errors";
|
|
5
|
-
import { parseQueryOptions } from "./query-parser";
|
|
6
|
-
import { httpMethodToOperation, isOperationAllowed } from "../../auth/api-keys/api-key-permission-guard";
|
|
7
|
-
import type { ApiKeyMasked } from "../../auth/api-keys/api-key-types";
|
|
8
|
-
import { logger } from "../../utils/logger";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Lightweight REST API generator that leverages existing Rebase DataDriver.
|
|
13
|
-
* Supports `include` query parameter for eager-loading relations via Drizzle.
|
|
14
|
-
*/
|
|
15
|
-
export class RestApiGenerator {
|
|
16
|
-
private collections: EntityCollection[];
|
|
17
|
-
private router: Hono<HonoEnv>;
|
|
18
|
-
private driver: DataDriver;
|
|
19
|
-
private dataHooks?: DataHooks;
|
|
20
|
-
private authAdapter?: AuthAdapter;
|
|
21
|
-
|
|
22
|
-
constructor(
|
|
23
|
-
collections: EntityCollection[],
|
|
24
|
-
driver: DataDriver,
|
|
25
|
-
dataHooks?: DataHooks,
|
|
26
|
-
authAdapter?: AuthAdapter
|
|
27
|
-
) {
|
|
28
|
-
this.collections = collections;
|
|
29
|
-
this.driver = driver;
|
|
30
|
-
this.dataHooks = dataHooks;
|
|
31
|
-
this.authAdapter = authAdapter;
|
|
32
|
-
this.router = new Hono<HonoEnv>();
|
|
33
|
-
}
|
|
34
|
-
|
|
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
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Generate REST routes using existing DataDriver
|
|
47
|
-
*/
|
|
48
|
-
generateRoutes(): Hono<HonoEnv> {
|
|
49
|
-
this.collections.forEach(collection => {
|
|
50
|
-
this.createCollectionRoutes(collection);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Catch-all routes for subcollection paths like
|
|
54
|
-
// /authors/111094/posts and /authors/111094/posts/43
|
|
55
|
-
// The DataDriver already knows how to resolve nested relation paths.
|
|
56
|
-
this.createSubcollectionRoutes();
|
|
57
|
-
|
|
58
|
-
return this.router;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Check API key permissions for a collection operation.
|
|
63
|
-
* Throws 403 if the key doesn't have the required permission.
|
|
64
|
-
* No-ops if the request is not authenticated via an API key.
|
|
65
|
-
*/
|
|
66
|
-
private enforceApiKeyPermission(
|
|
67
|
-
c: { get: (key: string) => unknown; req: { method: string } },
|
|
68
|
-
collectionSlug: string
|
|
69
|
-
): void {
|
|
70
|
-
const apiKey = c.get("apiKey") as ApiKeyMasked | undefined;
|
|
71
|
-
if (!apiKey) return; // Not an API key request — skip
|
|
72
|
-
|
|
73
|
-
const operation = httpMethodToOperation(c.req.method);
|
|
74
|
-
if (!isOperationAllowed(apiKey.permissions, collectionSlug, operation)) {
|
|
75
|
-
throw ApiError.forbidden(
|
|
76
|
-
`API key does not have "${operation}" permission for collection "${collectionSlug}"`,
|
|
77
|
-
"API_KEY_FORBIDDEN"
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
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
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Get the typed RestFetchService from a driver if it exposes one (for include support).
|
|
94
|
-
*/
|
|
95
|
-
private getFetchService(driver: DataDriver): RestFetchService | undefined {
|
|
96
|
-
return driver.restFetchService;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Create REST routes for a collection using existing Rebase patterns
|
|
101
|
-
*/
|
|
102
|
-
private createCollectionRoutes(collection: EntityCollection): void {
|
|
103
|
-
const basePath = `/${collection.slug}`;
|
|
104
|
-
const resolvedCollection = collection;
|
|
105
|
-
|
|
106
|
-
// GET /collection/count - Count entities (with optional filters)
|
|
107
|
-
this.router.get(`${basePath}/count`, async (c) => {
|
|
108
|
-
this.enforceApiKeyPermission(c, collection.slug);
|
|
109
|
-
const queryDict = c.req.queries();
|
|
110
|
-
const queryOptions = parseQueryOptions(queryDict);
|
|
111
|
-
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : undefined;
|
|
112
|
-
const driver = this.getScopedDriver(c);
|
|
113
|
-
|
|
114
|
-
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
|
|
115
|
-
return c.json({ count: total });
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// GET /collection - List entities
|
|
119
|
-
this.router.get(basePath, async (c) => {
|
|
120
|
-
this.enforceApiKeyPermission(c, collection.slug);
|
|
121
|
-
const queryDict = c.req.queries();
|
|
122
|
-
const queryOptions = parseQueryOptions(queryDict);
|
|
123
|
-
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : undefined;
|
|
124
|
-
|
|
125
|
-
const driver = this.getScopedDriver(c);
|
|
126
|
-
const fetchService = this.getFetchService(driver);
|
|
127
|
-
const hookCtx = this.buildHookContext(c, "GET");
|
|
128
|
-
|
|
129
|
-
// Use include-aware path when available
|
|
130
|
-
if (fetchService) {
|
|
131
|
-
const collectionPath = collection.slug;
|
|
132
|
-
let entities = await fetchService.fetchCollectionForRest(
|
|
133
|
-
collectionPath,
|
|
134
|
-
{
|
|
135
|
-
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
136
|
-
limit: queryOptions.limit,
|
|
137
|
-
offset: queryOptions.offset,
|
|
138
|
-
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
139
|
-
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
140
|
-
searchString,
|
|
141
|
-
vectorSearch: queryOptions.vectorSearch
|
|
142
|
-
},
|
|
143
|
-
queryOptions.include
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
entities = await this.applyAfterReadBatch(collection.slug, entities, hookCtx);
|
|
147
|
-
|
|
148
|
-
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
|
|
149
|
-
|
|
150
|
-
return c.json({
|
|
151
|
-
data: entities,
|
|
152
|
-
meta: {
|
|
153
|
-
total,
|
|
154
|
-
limit: queryOptions.limit,
|
|
155
|
-
offset: queryOptions.offset,
|
|
156
|
-
hasMore: (queryOptions.offset || 0) + entities.length < total
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Fallback path
|
|
162
|
-
let entities = await this.fetchRawCollection(driver, resolvedCollection, queryOptions, searchString);
|
|
163
|
-
|
|
164
|
-
entities = await this.applyAfterReadBatch(collection.slug, entities, hookCtx);
|
|
165
|
-
|
|
166
|
-
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
|
|
167
|
-
|
|
168
|
-
return c.json({
|
|
169
|
-
data: entities,
|
|
170
|
-
meta: {
|
|
171
|
-
total,
|
|
172
|
-
limit: queryOptions.limit,
|
|
173
|
-
offset: queryOptions.offset,
|
|
174
|
-
hasMore: (queryOptions.offset || 0) + entities.length < total
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// GET /collection/:id - Get single entity
|
|
180
|
-
this.router.get(`${basePath}/:id`, async (c) => {
|
|
181
|
-
this.enforceApiKeyPermission(c, collection.slug);
|
|
182
|
-
const id = c.req.param("id");
|
|
183
|
-
const queryDict = c.req.queries();
|
|
184
|
-
const queryOptions = parseQueryOptions(queryDict);
|
|
185
|
-
const driver = this.getScopedDriver(c);
|
|
186
|
-
const fetchService = this.getFetchService(driver);
|
|
187
|
-
const hookCtx = this.buildHookContext(c, "GET");
|
|
188
|
-
|
|
189
|
-
// Use include-aware path when available
|
|
190
|
-
if (fetchService) {
|
|
191
|
-
const collectionPath = collection.slug;
|
|
192
|
-
let entity = await fetchService.fetchEntityForRest(
|
|
193
|
-
collectionPath,
|
|
194
|
-
String(id),
|
|
195
|
-
queryOptions.include
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
if (!entity) {
|
|
199
|
-
throw ApiError.notFound("Entity not found");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
entity = await this.applyAfterRead(collection.slug, entity, hookCtx);
|
|
203
|
-
if (!entity) {
|
|
204
|
-
throw ApiError.notFound("Entity not found");
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return c.json(entity);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Fallback
|
|
211
|
-
let entity = await this.fetchRawEntity(driver, resolvedCollection, String(id));
|
|
212
|
-
|
|
213
|
-
if (!entity) {
|
|
214
|
-
throw ApiError.notFound("Entity not found");
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
entity = await this.applyAfterRead(collection.slug, entity, hookCtx);
|
|
218
|
-
if (!entity) {
|
|
219
|
-
throw ApiError.notFound("Entity not found");
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return c.json(entity);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// POST /collection - Create entity
|
|
226
|
-
this.router.post(basePath, async (c) => {
|
|
227
|
-
try {
|
|
228
|
-
this.enforceApiKeyPermission(c, collection.slug);
|
|
229
|
-
const driver = this.getScopedDriver(c);
|
|
230
|
-
const path = collection.slug;
|
|
231
|
-
const hookCtx = this.buildHookContext(c, "POST");
|
|
232
|
-
|
|
233
|
-
let body = await c.req.json().catch(() => ({}));
|
|
234
|
-
|
|
235
|
-
if (this.dataHooks?.beforeSave) {
|
|
236
|
-
body = await this.dataHooks.beforeSave(path, body, undefined, hookCtx);
|
|
237
|
-
}
|
|
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
|
-
|
|
279
|
-
const entity = await driver.saveEntity({
|
|
280
|
-
path,
|
|
281
|
-
values: body,
|
|
282
|
-
collection: resolvedCollection,
|
|
283
|
-
status: "new"
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
const response = this.formatResponse(entity);
|
|
287
|
-
|
|
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
|
-
}
|
|
293
|
-
|
|
294
|
-
return c.json(response, 201);
|
|
295
|
-
} catch (error) {
|
|
296
|
-
if (isRebaseApiError(error) && !error.code) {
|
|
297
|
-
// Only classify as BAD_REQUEST if it's an operational error
|
|
298
|
-
// (e.g. validation, DB constraints). Runtime bugs like TypeError,
|
|
299
|
-
// RangeError etc. should remain as 500 INTERNAL_ERROR.
|
|
300
|
-
const isRuntimeBug = error instanceof TypeError
|
|
301
|
-
|| error instanceof RangeError
|
|
302
|
-
|| error instanceof SyntaxError
|
|
303
|
-
|| error instanceof ReferenceError;
|
|
304
|
-
if (!isRuntimeBug) {
|
|
305
|
-
error.code = "BAD_REQUEST";
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
throw error;
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
// PUT /collection/:id - Update entity
|
|
313
|
-
this.router.put(`${basePath}/:id`, async (c) => {
|
|
314
|
-
try {
|
|
315
|
-
this.enforceApiKeyPermission(c, collection.slug);
|
|
316
|
-
const id = c.req.param("id");
|
|
317
|
-
const driver = this.getScopedDriver(c);
|
|
318
|
-
const hookCtx = this.buildHookContext(c, "PUT");
|
|
319
|
-
|
|
320
|
-
const existingEntity = await driver.fetchEntity({
|
|
321
|
-
path: collection.slug,
|
|
322
|
-
entityId: String(id),
|
|
323
|
-
collection: resolvedCollection
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
if (!existingEntity) {
|
|
327
|
-
throw ApiError.notFound("Entity not found");
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
let body = await c.req.json().catch(() => ({}));
|
|
331
|
-
|
|
332
|
-
if (this.dataHooks?.beforeSave) {
|
|
333
|
-
body = await this.dataHooks.beforeSave(collection.slug, body, String(id), hookCtx);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const entity = await driver.saveEntity({
|
|
337
|
-
path: collection.slug,
|
|
338
|
-
entityId: String(id),
|
|
339
|
-
values: body,
|
|
340
|
-
collection: resolvedCollection,
|
|
341
|
-
status: "existing"
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
const response = this.formatResponse(entity);
|
|
345
|
-
|
|
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
|
-
}
|
|
351
|
-
|
|
352
|
-
return c.json(response);
|
|
353
|
-
} catch (error) {
|
|
354
|
-
if (isRebaseApiError(error) && !error.code) {
|
|
355
|
-
// Only classify as BAD_REQUEST if it's an operational error.
|
|
356
|
-
// Runtime bugs (TypeError, RangeError, etc.) stay as 500.
|
|
357
|
-
const isRuntimeBug = error instanceof TypeError
|
|
358
|
-
|| error instanceof RangeError
|
|
359
|
-
|| error instanceof SyntaxError
|
|
360
|
-
|| error instanceof ReferenceError;
|
|
361
|
-
if (!isRuntimeBug) {
|
|
362
|
-
error.code = "BAD_REQUEST";
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
throw error;
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
// DELETE /collection/:id - Delete entity
|
|
370
|
-
this.router.delete(`${basePath}/:id`, async (c) => {
|
|
371
|
-
this.enforceApiKeyPermission(c, collection.slug);
|
|
372
|
-
const id = c.req.param("id");
|
|
373
|
-
const driver = this.getScopedDriver(c);
|
|
374
|
-
const hookCtx = this.buildHookContext(c, "DELETE");
|
|
375
|
-
|
|
376
|
-
const existingEntity = await driver.fetchEntity({
|
|
377
|
-
path: collection.slug,
|
|
378
|
-
entityId: String(id),
|
|
379
|
-
collection: resolvedCollection
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
if (!existingEntity) {
|
|
383
|
-
throw ApiError.notFound("Entity not found");
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (this.dataHooks?.beforeDelete) {
|
|
387
|
-
await this.dataHooks.beforeDelete(collection.slug, String(id), hookCtx);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
await driver.deleteEntity({
|
|
391
|
-
entity: existingEntity,
|
|
392
|
-
collection: resolvedCollection
|
|
393
|
-
});
|
|
394
|
-
|
|
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
|
-
}
|
|
400
|
-
|
|
401
|
-
return new Response(null, { status: 204 });
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Catch-all routes for subcollection paths.
|
|
407
|
-
*
|
|
408
|
-
* Matches URL patterns like:
|
|
409
|
-
* GET /authors/111094/posts → list child collection
|
|
410
|
-
* GET /authors/111094/posts/43 → get child entity
|
|
411
|
-
* POST /authors/111094/posts → create child entity
|
|
412
|
-
* PUT /authors/111094/posts/43 → update child entity
|
|
413
|
-
* DELETE /authors/111094/posts/43 → delete child entity
|
|
414
|
-
*
|
|
415
|
-
* The `:rest{.+}` regex param captures the full remainder of the URL
|
|
416
|
-
* path (Hono v4 `*` wildcard does not populate `c.req.param("*")`).
|
|
417
|
-
* We split it into segments and reconstruct the `collectionPath`
|
|
418
|
-
* (e.g. "authors/111094/posts") and optional `entityId` (e.g. "43").
|
|
419
|
-
*
|
|
420
|
-
* The DataDriver.saveEntity / fetchCollection / etc. already know how to
|
|
421
|
-
* resolve multi-segment relation paths, so we just forward to them.
|
|
422
|
-
*/
|
|
423
|
-
private createSubcollectionRoutes(): void {
|
|
424
|
-
// Reserved path segments that should NOT be treated as relation names.
|
|
425
|
-
// These are handled by dedicated route handlers (e.g., history routes)
|
|
426
|
-
// mounted on the same data router.
|
|
427
|
-
const RESERVED_SEGMENTS = new Set(["history"]);
|
|
428
|
-
|
|
429
|
-
// Helper: parse a path like "authors/111094/posts/43" into
|
|
430
|
-
// { collectionPath: "authors/111094/posts", entityId: "43" }
|
|
431
|
-
// or "authors/111094/posts" into
|
|
432
|
-
// { collectionPath: "authors/111094/posts", entityId: undefined }
|
|
433
|
-
const parseSubPath = (rawPath: string): { collectionPath: string; entityId?: string } | null => {
|
|
434
|
-
const segments = rawPath.split("/").filter(s => s && s !== "undefined");
|
|
435
|
-
// Need at least 3 segments for a subcollection path (parent/id/child)
|
|
436
|
-
if (segments.length < 3) return null;
|
|
437
|
-
|
|
438
|
-
// If any segment is a reserved path (e.g. "history"), this is not a
|
|
439
|
-
// subcollection route — let it fall through to other handlers.
|
|
440
|
-
if (segments.some(s => RESERVED_SEGMENTS.has(s))) return null;
|
|
441
|
-
|
|
442
|
-
// Odd segment count → collection path (parent/id/child or parent/id/child/id2/grandchild)
|
|
443
|
-
// Even segment count → entity path (parent/id/child/entityId)
|
|
444
|
-
if (segments.length % 2 === 1) {
|
|
445
|
-
return { collectionPath: segments.join("/") };
|
|
446
|
-
} else {
|
|
447
|
-
const entityId = segments.pop()!;
|
|
448
|
-
return { collectionPath: segments.join("/"),
|
|
449
|
-
entityId };
|
|
450
|
-
}
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
// GET /<subcollection-path> — list or get single entity
|
|
454
|
-
// Use :rest{.+} instead of * because Hono v4's wildcard doesn't
|
|
455
|
-
// capture into c.req.param("*") — it always returns undefined.
|
|
456
|
-
this.router.get("/:parent/:parentId/:rest{.+}", async (c, next) => {
|
|
457
|
-
const rest = c.req.param("rest");
|
|
458
|
-
if (!rest || rest === "undefined") return next();
|
|
459
|
-
const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
|
|
460
|
-
const parsed = parseSubPath(rawPath);
|
|
461
|
-
if (!parsed) return next();
|
|
462
|
-
|
|
463
|
-
const driver = this.getScopedDriver(c);
|
|
464
|
-
|
|
465
|
-
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
466
|
-
|
|
467
|
-
const hookCtx = this.buildHookContext(c, "GET");
|
|
468
|
-
|
|
469
|
-
if (parsed.entityId === "count") {
|
|
470
|
-
// GET /parent/:parentId/child/count — count child entities
|
|
471
|
-
const queryDict = c.req.queries();
|
|
472
|
-
const queryOptions = parseQueryOptions(queryDict);
|
|
473
|
-
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : undefined;
|
|
474
|
-
|
|
475
|
-
const total = driver.countEntities ? await driver.countEntities({
|
|
476
|
-
path: parsed.collectionPath,
|
|
477
|
-
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
478
|
-
searchString
|
|
479
|
-
}) : 0;
|
|
480
|
-
|
|
481
|
-
return c.json({ count: total });
|
|
482
|
-
} else if (parsed.entityId) {
|
|
483
|
-
// GET /parent/:parentId/child/:id — single entity
|
|
484
|
-
const entity = await driver.fetchEntity({
|
|
485
|
-
path: parsed.collectionPath,
|
|
486
|
-
entityId: parsed.entityId
|
|
487
|
-
});
|
|
488
|
-
if (!entity) throw ApiError.notFound("Entity not found");
|
|
489
|
-
|
|
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);
|
|
495
|
-
} else {
|
|
496
|
-
// GET /parent/:parentId/child — list entities
|
|
497
|
-
const queryDict = c.req.queries();
|
|
498
|
-
const queryOptions = parseQueryOptions(queryDict);
|
|
499
|
-
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : undefined;
|
|
500
|
-
const entities = await driver.fetchCollection({
|
|
501
|
-
path: parsed.collectionPath,
|
|
502
|
-
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
503
|
-
limit: queryOptions.limit,
|
|
504
|
-
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
505
|
-
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
506
|
-
searchString
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
let flatEntities = entities.map(e => this.flattenEntity(e));
|
|
510
|
-
flatEntities = await this.applyAfterReadBatch(c.req.param("parent"), flatEntities, hookCtx);
|
|
511
|
-
|
|
512
|
-
const total = driver.countEntities ? await driver.countEntities({
|
|
513
|
-
path: parsed.collectionPath,
|
|
514
|
-
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
515
|
-
searchString
|
|
516
|
-
}) : flatEntities.length;
|
|
517
|
-
|
|
518
|
-
return c.json({
|
|
519
|
-
data: flatEntities,
|
|
520
|
-
meta: {
|
|
521
|
-
total,
|
|
522
|
-
limit: queryOptions.limit,
|
|
523
|
-
offset: queryOptions.offset,
|
|
524
|
-
hasMore: (queryOptions.offset || 0) + flatEntities.length < total
|
|
525
|
-
}
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
// POST /<subcollection-path> — create entity
|
|
531
|
-
this.router.post("/:parent/:parentId/:rest{.+}", async (c, next) => {
|
|
532
|
-
const rest = c.req.param("rest");
|
|
533
|
-
if (!rest || rest === "undefined") return next();
|
|
534
|
-
const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
|
|
535
|
-
const parsed = parseSubPath(rawPath);
|
|
536
|
-
if (!parsed || parsed.entityId) return next();
|
|
537
|
-
|
|
538
|
-
const driver = this.getScopedDriver(c);
|
|
539
|
-
const hookCtx = this.buildHookContext(c, "POST");
|
|
540
|
-
|
|
541
|
-
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
542
|
-
let body = await c.req.json().catch(() => ({}));
|
|
543
|
-
|
|
544
|
-
if (this.dataHooks?.beforeSave) {
|
|
545
|
-
body = await this.dataHooks.beforeSave(parsed.collectionPath, body, undefined, hookCtx);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const entity = await driver.saveEntity({
|
|
549
|
-
path: parsed.collectionPath,
|
|
550
|
-
values: body,
|
|
551
|
-
status: "new"
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
const response = this.formatResponse(entity);
|
|
555
|
-
|
|
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
|
-
}
|
|
561
|
-
|
|
562
|
-
return c.json(response, 201);
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
// PUT /<subcollection-path>/:id — update entity
|
|
566
|
-
this.router.put("/:parent/:parentId/:rest{.+}", async (c, next) => {
|
|
567
|
-
const rest = c.req.param("rest");
|
|
568
|
-
if (!rest || rest === "undefined") return next();
|
|
569
|
-
const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
|
|
570
|
-
const parsed = parseSubPath(rawPath);
|
|
571
|
-
if (!parsed || !parsed.entityId) return next();
|
|
572
|
-
|
|
573
|
-
const driver = this.getScopedDriver(c);
|
|
574
|
-
const hookCtx = this.buildHookContext(c, "PUT");
|
|
575
|
-
|
|
576
|
-
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
577
|
-
|
|
578
|
-
let body = await c.req.json().catch(() => ({}));
|
|
579
|
-
|
|
580
|
-
if (this.dataHooks?.beforeSave) {
|
|
581
|
-
body = await this.dataHooks.beforeSave(parsed.collectionPath, body, parsed.entityId, hookCtx);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
const entity = await driver.saveEntity({
|
|
585
|
-
path: parsed.collectionPath,
|
|
586
|
-
entityId: parsed.entityId,
|
|
587
|
-
values: body,
|
|
588
|
-
status: "existing"
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
const response = this.formatResponse(entity);
|
|
592
|
-
|
|
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
|
-
}
|
|
598
|
-
|
|
599
|
-
return c.json(response);
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
// DELETE /<subcollection-path>/:id — delete entity
|
|
603
|
-
this.router.delete("/:parent/:parentId/:rest{.+}", async (c, next) => {
|
|
604
|
-
const rest = c.req.param("rest");
|
|
605
|
-
if (!rest || rest === "undefined") return next();
|
|
606
|
-
const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
|
|
607
|
-
const parsed = parseSubPath(rawPath);
|
|
608
|
-
if (!parsed || !parsed.entityId) return next();
|
|
609
|
-
|
|
610
|
-
const driver = this.getScopedDriver(c);
|
|
611
|
-
const hookCtx = this.buildHookContext(c, "DELETE");
|
|
612
|
-
|
|
613
|
-
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
614
|
-
|
|
615
|
-
const existingEntity = await driver.fetchEntity({
|
|
616
|
-
path: parsed.collectionPath,
|
|
617
|
-
entityId: parsed.entityId
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
if (!existingEntity) throw ApiError.notFound("Entity not found");
|
|
621
|
-
|
|
622
|
-
if (this.dataHooks?.beforeDelete) {
|
|
623
|
-
await this.dataHooks.beforeDelete(parsed.collectionPath, parsed.entityId, hookCtx);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
await driver.deleteEntity({ entity: existingEntity });
|
|
627
|
-
|
|
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
|
-
}
|
|
633
|
-
|
|
634
|
-
return new Response(null, { status: 204 });
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Format successful API response - flattened for traditional REST API
|
|
640
|
-
*/
|
|
641
|
-
private formatResponse<T>(data: T, meta?: Record<string, unknown>): unknown {
|
|
642
|
-
if (Array.isArray(data)) {
|
|
643
|
-
const flattenedData = data.map(entity => this.flattenEntity(entity));
|
|
644
|
-
if (meta) {
|
|
645
|
-
return {
|
|
646
|
-
data: flattenedData,
|
|
647
|
-
meta
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
return flattenedData;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
if (data && typeof data === "object" && "values" in data) {
|
|
654
|
-
return this.flattenEntity(data as unknown as Entity<Record<string, unknown>>);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
if (meta) {
|
|
658
|
-
return {
|
|
659
|
-
data,
|
|
660
|
-
meta
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
return data;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
/**
|
|
667
|
-
* Flatten Rebase entity structure to traditional REST format
|
|
668
|
-
*/
|
|
669
|
-
private flattenEntity(entity: Entity<Record<string, unknown>>): Record<string, unknown> {
|
|
670
|
-
if (!entity || typeof entity !== "object") {
|
|
671
|
-
return entity;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
if ("values" in entity && typeof entity.values === "object") {
|
|
675
|
-
return {
|
|
676
|
-
id: entity.id,
|
|
677
|
-
...entity.values
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
return entity as unknown as Record<string, unknown>;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
* Fetch raw collection data without Entity wrapper (fallback for non-Postgres)
|
|
686
|
-
*/
|
|
687
|
-
private async fetchRawCollection(driver: DataDriver, collection: EntityCollection, queryOptions: QueryOptions, searchString?: string) {
|
|
688
|
-
const entities = await driver.fetchCollection({
|
|
689
|
-
path: collection.slug,
|
|
690
|
-
collection,
|
|
691
|
-
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
692
|
-
limit: queryOptions.limit,
|
|
693
|
-
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
694
|
-
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
695
|
-
startAfter: queryOptions.offset ? String(queryOptions.offset) : undefined,
|
|
696
|
-
searchString,
|
|
697
|
-
vectorSearch: queryOptions.vectorSearch
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
return entities.map(entity => this.flattenEntity(entity));
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
/**
|
|
704
|
-
* Count raw entities for a collection
|
|
705
|
-
*/
|
|
706
|
-
private async countRawEntities(driver: DataDriver, collection: EntityCollection, queryOptions: QueryOptions, searchString?: string): Promise<number> {
|
|
707
|
-
return driver.countEntities ? await driver.countEntities({
|
|
708
|
-
path: collection.slug,
|
|
709
|
-
collection,
|
|
710
|
-
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
711
|
-
searchString
|
|
712
|
-
}) : 0;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/**
|
|
716
|
-
* Fetch single entity raw data without Entity wrapper (fallback)
|
|
717
|
-
*/
|
|
718
|
-
private async fetchRawEntity(driver: DataDriver, collection: EntityCollection, entityId: string) {
|
|
719
|
-
const entity = await driver.fetchEntity({
|
|
720
|
-
path: collection.slug,
|
|
721
|
-
entityId,
|
|
722
|
-
collection
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
return entity ? this.flattenEntity(entity) : null;
|
|
726
|
-
}
|
|
727
|
-
|
|
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
|
-
|
|
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
|
-
}
|