@rebasepro/server-core 0.0.1-canary.000dc36
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/LICENSE +6 -0
- package/README.md +40 -0
- package/build-errors.txt +52 -0
- package/coverage/clover.xml +3739 -0
- package/coverage/coverage-final.json +31 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +266 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +952 -0
- package/coverage/lcov-report/src/api/errors.ts.html +472 -0
- package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +1069 -0
- package/coverage/lcov-report/src/api/graphql/index.html +116 -0
- package/coverage/lcov-report/src/api/index.html +176 -0
- package/coverage/lcov-report/src/api/openapi-generator.ts.html +565 -0
- package/coverage/lcov-report/src/api/rest/api-generator.ts.html +994 -0
- package/coverage/lcov-report/src/api/rest/index.html +131 -0
- package/coverage/lcov-report/src/api/rest/query-parser.ts.html +550 -0
- package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +202 -0
- package/coverage/lcov-report/src/api/server.ts.html +823 -0
- package/coverage/lcov-report/src/auth/admin-routes.ts.html +973 -0
- package/coverage/lcov-report/src/auth/index.html +176 -0
- package/coverage/lcov-report/src/auth/jwt.ts.html +574 -0
- package/coverage/lcov-report/src/auth/middleware.ts.html +745 -0
- package/coverage/lcov-report/src/auth/password.ts.html +310 -0
- package/coverage/lcov-report/src/auth/services.ts.html +2074 -0
- package/coverage/lcov-report/src/collections/index.html +116 -0
- package/coverage/lcov-report/src/collections/loader.ts.html +232 -0
- package/coverage/lcov-report/src/db/auth-schema.ts.html +523 -0
- package/coverage/lcov-report/src/db/data-transformer.ts.html +1753 -0
- package/coverage/lcov-report/src/db/entityService.ts.html +700 -0
- package/coverage/lcov-report/src/db/index.html +146 -0
- package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +4048 -0
- package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +883 -0
- package/coverage/lcov-report/src/db/services/RelationService.ts.html +3121 -0
- package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +442 -0
- package/coverage/lcov-report/src/db/services/index.html +176 -0
- package/coverage/lcov-report/src/db/services/index.ts.html +124 -0
- package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +1960 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/services/driver-registry.ts.html +631 -0
- package/coverage/lcov-report/src/services/index.html +131 -0
- package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +3025 -0
- package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +1189 -0
- package/coverage/lcov-report/src/storage/S3StorageController.ts.html +970 -0
- package/coverage/lcov-report/src/storage/index.html +161 -0
- package/coverage/lcov-report/src/storage/storage-registry.ts.html +646 -0
- package/coverage/lcov-report/src/storage/types.ts.html +451 -0
- package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +3082 -0
- package/coverage/lcov-report/src/utils/index.html +116 -0
- package/coverage/lcov.info +7179 -0
- package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/buildRebaseData.d.ts +14 -0
- package/dist/common/src/index.d.ts +3 -0
- package/dist/common/src/util/builders.d.ts +57 -0
- package/dist/common/src/util/callbacks.d.ts +6 -0
- package/dist/common/src/util/collections.d.ts +11 -0
- package/dist/common/src/util/common.d.ts +2 -0
- package/dist/common/src/util/conditions.d.ts +26 -0
- package/dist/common/src/util/entities.d.ts +58 -0
- package/dist/common/src/util/enums.d.ts +3 -0
- package/dist/common/src/util/index.d.ts +16 -0
- package/dist/common/src/util/navigation_from_path.d.ts +34 -0
- package/dist/common/src/util/navigation_utils.d.ts +20 -0
- package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
- package/dist/common/src/util/paths.d.ts +14 -0
- package/dist/common/src/util/permissions.d.ts +5 -0
- package/dist/common/src/util/references.d.ts +2 -0
- package/dist/common/src/util/relations.d.ts +22 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index-DXVBFp5V.js +37 -0
- package/dist/index-DXVBFp5V.js.map +1 -0
- package/dist/index.es.js +49249 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +49283 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/server-core/src/api/ast-schema-editor.d.ts +21 -0
- package/dist/server-core/src/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
- package/dist/server-core/src/api/errors.d.ts +35 -0
- package/dist/server-core/src/api/graphql/graphql-schema-generator.d.ts +35 -0
- package/dist/server-core/src/api/graphql/index.d.ts +1 -0
- package/dist/server-core/src/api/index.d.ts +9 -0
- package/dist/server-core/src/api/openapi-generator.d.ts +16 -0
- package/dist/server-core/src/api/rest/api-generator.d.ts +76 -0
- package/dist/server-core/src/api/rest/index.d.ts +1 -0
- package/dist/server-core/src/api/rest/query-parser.d.ts +9 -0
- package/dist/server-core/src/api/schema-editor-routes.d.ts +3 -0
- package/dist/server-core/src/api/server.d.ts +40 -0
- package/dist/server-core/src/api/types.d.ts +90 -0
- package/dist/server-core/src/auth/admin-routes.d.ts +21 -0
- package/dist/server-core/src/auth/apple-oauth.d.ts +30 -0
- package/dist/server-core/src/auth/bitbucket-oauth.d.ts +11 -0
- package/dist/server-core/src/auth/discord-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/facebook-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/github-oauth.d.ts +15 -0
- package/dist/server-core/src/auth/gitlab-oauth.d.ts +13 -0
- package/dist/server-core/src/auth/google-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/index.d.ts +23 -0
- package/dist/server-core/src/auth/interfaces.d.ts +309 -0
- package/dist/server-core/src/auth/jwt.d.ts +43 -0
- package/dist/server-core/src/auth/linkedin-oauth.d.ts +18 -0
- package/dist/server-core/src/auth/microsoft-oauth.d.ts +16 -0
- package/dist/server-core/src/auth/middleware.d.ts +81 -0
- package/dist/server-core/src/auth/password.d.ts +22 -0
- package/dist/server-core/src/auth/rate-limiter.d.ts +31 -0
- package/dist/server-core/src/auth/routes.d.ts +27 -0
- package/dist/server-core/src/auth/slack-oauth.d.ts +12 -0
- package/dist/server-core/src/auth/spotify-oauth.d.ts +12 -0
- package/dist/server-core/src/auth/twitter-oauth.d.ts +18 -0
- package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
- package/dist/server-core/src/collections/BackendCollectionRegistry.d.ts +13 -0
- package/dist/server-core/src/collections/loader.d.ts +5 -0
- package/dist/server-core/src/cron/cron-loader.d.ts +17 -0
- package/dist/server-core/src/cron/cron-routes.d.ts +14 -0
- package/dist/server-core/src/cron/cron-scheduler.d.ts +106 -0
- package/dist/server-core/src/cron/cron-store.d.ts +32 -0
- package/dist/server-core/src/cron/index.d.ts +6 -0
- package/dist/server-core/src/db/interfaces.d.ts +18 -0
- package/dist/server-core/src/email/index.d.ts +6 -0
- package/dist/server-core/src/email/smtp-email-service.d.ts +25 -0
- package/dist/server-core/src/email/templates.d.ts +42 -0
- package/dist/server-core/src/email/types.d.ts +107 -0
- package/dist/server-core/src/functions/function-loader.d.ts +17 -0
- package/dist/server-core/src/functions/function-routes.d.ts +10 -0
- package/dist/server-core/src/functions/index.d.ts +3 -0
- package/dist/server-core/src/history/history-routes.d.ts +23 -0
- package/dist/server-core/src/history/index.d.ts +1 -0
- package/dist/server-core/src/index.d.ts +29 -0
- package/dist/server-core/src/init.d.ts +168 -0
- package/dist/server-core/src/serve-spa.d.ts +30 -0
- package/dist/server-core/src/services/driver-registry.d.ts +78 -0
- package/dist/server-core/src/singleton.d.ts +35 -0
- package/dist/server-core/src/storage/LocalStorageController.d.ts +46 -0
- package/dist/server-core/src/storage/S3StorageController.d.ts +36 -0
- package/dist/server-core/src/storage/index.d.ts +25 -0
- package/dist/server-core/src/storage/routes.d.ts +38 -0
- package/dist/server-core/src/storage/storage-registry.d.ts +78 -0
- package/dist/server-core/src/storage/types.d.ts +103 -0
- package/dist/server-core/src/types/index.d.ts +11 -0
- package/dist/server-core/src/utils/dev-port.d.ts +35 -0
- package/dist/server-core/src/utils/logger.d.ts +31 -0
- package/dist/server-core/src/utils/logging.d.ts +9 -0
- package/dist/server-core/src/utils/request-logger.d.ts +19 -0
- package/dist/server-core/src/utils/sql.d.ts +27 -0
- package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
- package/dist/types/src/controllers/auth.d.ts +119 -0
- package/dist/types/src/controllers/client.d.ts +170 -0
- package/dist/types/src/controllers/collection_registry.d.ts +46 -0
- package/dist/types/src/controllers/customization_controller.d.ts +60 -0
- package/dist/types/src/controllers/data.d.ts +168 -0
- package/dist/types/src/controllers/data_driver.d.ts +195 -0
- package/dist/types/src/controllers/database_admin.d.ts +11 -0
- package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
- package/dist/types/src/controllers/effective_role.d.ts +4 -0
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +18 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
- package/dist/types/src/controllers/navigation.d.ts +213 -0
- package/dist/types/src/controllers/registry.d.ts +54 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +171 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +105 -0
- package/dist/types/src/types/backend.d.ts +536 -0
- package/dist/types/src/types/backend_hooks.d.ts +187 -0
- package/dist/types/src/types/builders.d.ts +15 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +857 -0
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +64 -0
- package/dist/types/src/types/entities.d.ts +145 -0
- package/dist/types/src/types/entity_actions.d.ts +98 -0
- package/dist/types/src/types/entity_callbacks.d.ts +173 -0
- package/dist/types/src/types/entity_link_builder.d.ts +7 -0
- package/dist/types/src/types/entity_overrides.d.ts +10 -0
- package/dist/types/src/types/entity_views.d.ts +59 -0
- package/dist/types/src/types/export_import.d.ts +21 -0
- package/dist/types/src/types/formex.d.ts +40 -0
- package/dist/types/src/types/index.d.ts +25 -0
- package/dist/types/src/types/locales.d.ts +4 -0
- package/dist/types/src/types/modify_collections.d.ts +5 -0
- package/dist/types/src/types/plugins.d.ts +282 -0
- package/dist/types/src/types/properties.d.ts +1148 -0
- package/dist/types/src/types/property_config.d.ts +70 -0
- package/dist/types/src/types/relations.d.ts +336 -0
- package/dist/types/src/types/slots.d.ts +262 -0
- package/dist/types/src/types/translations.d.ts +874 -0
- package/dist/types/src/types/user_management_delegate.d.ts +121 -0
- package/dist/types/src/types/websockets.d.ts +78 -0
- package/dist/types/src/users/index.d.ts +2 -0
- package/dist/types/src/users/roles.d.ts +22 -0
- package/dist/types/src/users/user.d.ts +46 -0
- package/history_diff.log +385 -0
- package/jest.config.cjs +16 -0
- package/package.json +86 -0
- package/scratch.ts +9 -0
- package/src/api/ast-schema-editor.ts +289 -0
- package/src/api/collections_for_test/callbacks_test_collection.ts +60 -0
- package/src/api/errors.ts +179 -0
- package/src/api/graphql/graphql-schema-generator.ts +336 -0
- package/src/api/graphql/index.ts +2 -0
- package/src/api/index.ts +11 -0
- package/src/api/openapi-generator.ts +715 -0
- package/src/api/rest/api-generator-count.test.ts +113 -0
- package/src/api/rest/api-generator.ts +573 -0
- package/src/api/rest/index.ts +2 -0
- package/src/api/rest/query-parser.ts +155 -0
- package/src/api/schema-editor-routes.ts +41 -0
- package/src/api/server.ts +249 -0
- package/src/api/types.ts +90 -0
- package/src/auth/admin-routes.ts +605 -0
- package/src/auth/apple-oauth.ts +120 -0
- package/src/auth/bitbucket-oauth.ts +82 -0
- package/src/auth/discord-oauth.ts +83 -0
- package/src/auth/facebook-oauth.ts +72 -0
- package/src/auth/github-oauth.ts +110 -0
- package/src/auth/gitlab-oauth.ts +70 -0
- package/src/auth/google-oauth.ts +48 -0
- package/src/auth/index.ts +34 -0
- package/src/auth/interfaces.ts +363 -0
- package/src/auth/jwt.ts +181 -0
- package/src/auth/linkedin-oauth.ts +81 -0
- package/src/auth/microsoft-oauth.ts +88 -0
- package/src/auth/middleware.ts +384 -0
- package/src/auth/password.ts +77 -0
- package/src/auth/rate-limiter.ts +133 -0
- package/src/auth/routes.ts +788 -0
- package/src/auth/slack-oauth.ts +71 -0
- package/src/auth/spotify-oauth.ts +67 -0
- package/src/auth/twitter-oauth.ts +120 -0
- package/src/bootstrappers/index.ts +1 -0
- package/src/collections/BackendCollectionRegistry.ts +20 -0
- package/src/collections/loader.ts +49 -0
- package/src/cron/cron-loader.ts +89 -0
- package/src/cron/cron-routes.test.ts +265 -0
- package/src/cron/cron-routes.ts +85 -0
- package/src/cron/cron-scheduler.test.ts +547 -0
- package/src/cron/cron-scheduler.ts +576 -0
- package/src/cron/cron-store.ts +163 -0
- package/src/cron/index.ts +6 -0
- package/src/db/interfaces.ts +60 -0
- package/src/email/index.ts +18 -0
- package/src/email/smtp-email-service.ts +91 -0
- package/src/email/templates.ts +388 -0
- package/src/email/types.ts +105 -0
- package/src/functions/function-loader.ts +119 -0
- package/src/functions/function-routes.ts +31 -0
- package/src/functions/index.ts +3 -0
- package/src/history/history-routes.ts +129 -0
- package/src/history/index.ts +2 -0
- package/src/index.ts +66 -0
- package/src/init.ts +737 -0
- package/src/serve-spa.ts +81 -0
- package/src/services/driver-registry.ts +182 -0
- package/src/singleton.test.ts +28 -0
- package/src/singleton.ts +70 -0
- package/src/storage/LocalStorageController.ts +365 -0
- package/src/storage/S3StorageController.ts +298 -0
- package/src/storage/index.ts +43 -0
- package/src/storage/routes.ts +264 -0
- package/src/storage/storage-registry.ts +187 -0
- package/src/storage/types.ts +134 -0
- package/src/types/index.ts +27 -0
- package/src/utils/dev-port.ts +176 -0
- package/src/utils/logger.ts +143 -0
- package/src/utils/logging.ts +38 -0
- package/src/utils/request-logger.ts +66 -0
- package/src/utils/sql.ts +38 -0
- package/test/admin-routes.test.ts +640 -0
- package/test/api-generator.test.ts +501 -0
- package/test/ast-schema-editor.test.ts +63 -0
- package/test/auth-middleware-hono.test.ts +556 -0
- package/test/auth-routes.test.ts +1047 -0
- package/test/backend-hooks-admin.test.ts +394 -0
- package/test/backend-hooks-data.test.ts +408 -0
- package/test/driver-registry.test.ts +282 -0
- package/test/error-propagation.test.ts +226 -0
- package/test/errors-hono.test.ts +133 -0
- package/test/errors.test.ts +155 -0
- package/test/jwt-security.test.ts +182 -0
- package/test/jwt.test.ts +324 -0
- package/test/middleware.test.ts +300 -0
- package/test/password.test.ts +165 -0
- package/test/query-parser.test.ts +263 -0
- package/test/rate-limiter.test.ts +102 -0
- package/test/safe-compare.test.ts +66 -0
- package/test/singleton.test.ts +59 -0
- package/test/storage-local.test.ts +271 -0
- package/test/storage-registry.test.ts +282 -0
- package/test/storage-routes.test.ts +222 -0
- package/test/storage-s3.test.ts +304 -0
- package/test-ast.ts +28 -0
- package/test.ts +6 -0
- package/test_output.txt +1133 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- package/vite.config.ts +80 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { RestApiGenerator } from "./api-generator";
|
|
3
|
+
import type { DataDriver, EntityCollection, FetchCollectionProps } from "@rebasepro/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal mock DataDriver for testing.
|
|
7
|
+
*/
|
|
8
|
+
function createMockDriver(overrides?: Partial<DataDriver>): DataDriver {
|
|
9
|
+
return {
|
|
10
|
+
fetchCollection: jest.fn().mockResolvedValue([]),
|
|
11
|
+
fetchEntity: jest.fn().mockResolvedValue(null),
|
|
12
|
+
saveEntity: jest.fn().mockResolvedValue({ id: "1", path: "test", values: {} }),
|
|
13
|
+
deleteEntity: jest.fn().mockResolvedValue(undefined),
|
|
14
|
+
countEntities: jest.fn().mockResolvedValue(0),
|
|
15
|
+
...overrides,
|
|
16
|
+
} as unknown as DataDriver;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createTestCollection(slug: string): EntityCollection {
|
|
20
|
+
return {
|
|
21
|
+
slug,
|
|
22
|
+
name: slug.charAt(0).toUpperCase() + slug.slice(1),
|
|
23
|
+
path: slug,
|
|
24
|
+
properties: {},
|
|
25
|
+
} as unknown as EntityCollection;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("RestApiGenerator - Count Endpoint", () => {
|
|
29
|
+
let driver: DataDriver;
|
|
30
|
+
let collection: EntityCollection;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
driver = createMockDriver({
|
|
34
|
+
countEntities: jest.fn().mockResolvedValue(42),
|
|
35
|
+
});
|
|
36
|
+
collection = createTestCollection("products");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("GET /products/count should return a count object", async () => {
|
|
40
|
+
const generator = new RestApiGenerator([collection], driver);
|
|
41
|
+
const app = generator.generateRoutes();
|
|
42
|
+
|
|
43
|
+
const res = await app.request("/products/count");
|
|
44
|
+
expect(res.status).toBe(200);
|
|
45
|
+
|
|
46
|
+
const json = await res.json() as { count: number };
|
|
47
|
+
expect(json.count).toBe(42);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should pass filters to countEntities driver", async () => {
|
|
51
|
+
const generator = new RestApiGenerator([collection], driver);
|
|
52
|
+
const app = generator.generateRoutes();
|
|
53
|
+
|
|
54
|
+
const res = await app.request("/products/count?status=eq.active");
|
|
55
|
+
expect(res.status).toBe(200);
|
|
56
|
+
|
|
57
|
+
const json = await res.json() as { count: number };
|
|
58
|
+
expect(json.count).toBe(42);
|
|
59
|
+
|
|
60
|
+
// Verify countEntities was called with the filter
|
|
61
|
+
expect(driver.countEntities).toHaveBeenCalled();
|
|
62
|
+
const callArgs = (driver.countEntities as ReturnType<typeof jest.fn>).mock.calls[0][0] as FetchCollectionProps;
|
|
63
|
+
expect(callArgs.path).toBe("products");
|
|
64
|
+
expect(callArgs.filter).toHaveProperty("status");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should pass searchString to countEntities driver", async () => {
|
|
68
|
+
const generator = new RestApiGenerator([collection], driver);
|
|
69
|
+
const app = generator.generateRoutes();
|
|
70
|
+
|
|
71
|
+
const res = await app.request("/products/count?searchString=widget");
|
|
72
|
+
expect(res.status).toBe(200);
|
|
73
|
+
|
|
74
|
+
const json = await res.json() as { count: number };
|
|
75
|
+
expect(json.count).toBe(42);
|
|
76
|
+
|
|
77
|
+
expect(driver.countEntities).toHaveBeenCalled();
|
|
78
|
+
const callArgs = (driver.countEntities as ReturnType<typeof jest.fn>).mock.calls[0][0] as FetchCollectionProps;
|
|
79
|
+
expect(callArgs.searchString).toBe("widget");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should return 0 when countEntities is not available on driver", async () => {
|
|
83
|
+
const driverWithoutCount = createMockDriver({ countEntities: undefined });
|
|
84
|
+
const generator = new RestApiGenerator([collection], driverWithoutCount);
|
|
85
|
+
const app = generator.generateRoutes();
|
|
86
|
+
|
|
87
|
+
const res = await app.request("/products/count");
|
|
88
|
+
expect(res.status).toBe(200);
|
|
89
|
+
|
|
90
|
+
const json = await res.json() as { count: number };
|
|
91
|
+
expect(json.count).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("GET /products/count should not be confused with GET /products/:id", async () => {
|
|
95
|
+
// Ensure the count route is registered before the :id route
|
|
96
|
+
const fetchEntity = jest.fn().mockResolvedValue(null);
|
|
97
|
+
const driverCustom = createMockDriver({
|
|
98
|
+
countEntities: jest.fn().mockResolvedValue(99),
|
|
99
|
+
fetchEntity,
|
|
100
|
+
});
|
|
101
|
+
const generator = new RestApiGenerator([collection], driverCustom);
|
|
102
|
+
const app = generator.generateRoutes();
|
|
103
|
+
|
|
104
|
+
const res = await app.request("/products/count");
|
|
105
|
+
expect(res.status).toBe(200);
|
|
106
|
+
|
|
107
|
+
const json = await res.json() as { count: number };
|
|
108
|
+
expect(json.count).toBe(99);
|
|
109
|
+
|
|
110
|
+
// fetchEntity should NOT have been called (i.e. "count" was not treated as an entity ID)
|
|
111
|
+
expect(fetchEntity).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { DataDriver, Entity, EntityCollection, FetchCollectionProps, DataHooks, BackendHookContext, RestFetchService } from "@rebasepro/types";
|
|
3
|
+
import { QueryOptions, HonoEnv } from "../types";
|
|
4
|
+
import { ApiError } from "../errors";
|
|
5
|
+
import { parseQueryOptions } from "./query-parser";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Lightweight REST API generator that leverages existing Rebase DataDriver.
|
|
10
|
+
* Supports `include` query parameter for eager-loading relations via Drizzle.
|
|
11
|
+
*/
|
|
12
|
+
export class RestApiGenerator {
|
|
13
|
+
private collections: EntityCollection[];
|
|
14
|
+
private router: Hono<HonoEnv>;
|
|
15
|
+
private driver: DataDriver;
|
|
16
|
+
private dataHooks?: DataHooks;
|
|
17
|
+
|
|
18
|
+
constructor(collections: EntityCollection[], driver: DataDriver, dataHooks?: DataHooks) {
|
|
19
|
+
this.collections = collections;
|
|
20
|
+
this.driver = driver;
|
|
21
|
+
this.dataHooks = dataHooks;
|
|
22
|
+
this.router = new Hono<HonoEnv>();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Build a BackendHookContext from a Hono context */
|
|
26
|
+
private buildHookContext(c: { get: (key: string) => unknown }, method: BackendHookContext["method"]): BackendHookContext {
|
|
27
|
+
const user = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
28
|
+
return {
|
|
29
|
+
requestUser: user ? { userId: user.userId, roles: user.roles ?? [] } : undefined,
|
|
30
|
+
method
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate REST routes using existing DataDriver
|
|
36
|
+
*/
|
|
37
|
+
generateRoutes(): Hono<HonoEnv> {
|
|
38
|
+
this.collections.forEach(collection => {
|
|
39
|
+
this.createCollectionRoutes(collection);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Catch-all routes for subcollection paths like
|
|
43
|
+
// /authors/111094/posts and /authors/111094/posts/43
|
|
44
|
+
// The DataDriver already knows how to resolve nested relation paths.
|
|
45
|
+
this.createSubcollectionRoutes();
|
|
46
|
+
|
|
47
|
+
return this.router;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the typed RestFetchService from a driver if it exposes one (for include support).
|
|
52
|
+
*/
|
|
53
|
+
private getFetchService(driver: DataDriver): RestFetchService | undefined {
|
|
54
|
+
return driver.restFetchService;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create REST routes for a collection using existing Rebase patterns
|
|
59
|
+
*/
|
|
60
|
+
private createCollectionRoutes(collection: EntityCollection): void {
|
|
61
|
+
const basePath = `/${collection.slug}`;
|
|
62
|
+
const resolvedCollection = collection;
|
|
63
|
+
|
|
64
|
+
// GET /collection/count - Count entities (with optional filters)
|
|
65
|
+
this.router.get(`${basePath}/count`, async (c) => {
|
|
66
|
+
const queryDict = c.req.query();
|
|
67
|
+
const queryOptions = parseQueryOptions(queryDict);
|
|
68
|
+
const searchString = queryDict.searchString as string | undefined;
|
|
69
|
+
const driver = c.get("driver") || this.driver;
|
|
70
|
+
|
|
71
|
+
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
|
|
72
|
+
return c.json({ count: total });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// GET /collection - List entities
|
|
76
|
+
this.router.get(basePath, async (c) => {
|
|
77
|
+
const queryDict = c.req.query();
|
|
78
|
+
const queryOptions = parseQueryOptions(queryDict);
|
|
79
|
+
const searchString = queryDict.searchString as string | undefined;
|
|
80
|
+
|
|
81
|
+
const driver = c.get("driver") || this.driver;
|
|
82
|
+
const fetchService = this.getFetchService(driver);
|
|
83
|
+
const hookCtx = this.buildHookContext(c, "GET");
|
|
84
|
+
|
|
85
|
+
// Use include-aware path when available
|
|
86
|
+
if (fetchService) {
|
|
87
|
+
const collectionPath = collection.slug;
|
|
88
|
+
let entities = await fetchService.fetchCollectionForRest(
|
|
89
|
+
collectionPath,
|
|
90
|
+
{
|
|
91
|
+
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
92
|
+
limit: queryOptions.limit,
|
|
93
|
+
offset: queryOptions.offset,
|
|
94
|
+
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
95
|
+
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
96
|
+
searchString
|
|
97
|
+
},
|
|
98
|
+
queryOptions.include
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
entities = await this.applyAfterReadBatch(collection.slug, entities, hookCtx);
|
|
102
|
+
|
|
103
|
+
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
|
|
104
|
+
|
|
105
|
+
return c.json({
|
|
106
|
+
data: entities,
|
|
107
|
+
meta: {
|
|
108
|
+
total,
|
|
109
|
+
limit: queryOptions.limit,
|
|
110
|
+
offset: queryOptions.offset,
|
|
111
|
+
hasMore: (queryOptions.offset || 0) + entities.length < total
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Fallback path
|
|
117
|
+
let entities = await this.fetchRawCollection(driver, resolvedCollection, queryOptions, searchString);
|
|
118
|
+
|
|
119
|
+
entities = await this.applyAfterReadBatch(collection.slug, entities, hookCtx);
|
|
120
|
+
|
|
121
|
+
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
|
|
122
|
+
|
|
123
|
+
return c.json({
|
|
124
|
+
data: entities,
|
|
125
|
+
meta: {
|
|
126
|
+
total,
|
|
127
|
+
limit: queryOptions.limit,
|
|
128
|
+
offset: queryOptions.offset,
|
|
129
|
+
hasMore: (queryOptions.offset || 0) + entities.length < total
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// GET /collection/:id - Get single entity
|
|
135
|
+
this.router.get(`${basePath}/:id`, async (c) => {
|
|
136
|
+
const id = c.req.param("id");
|
|
137
|
+
const queryDict = c.req.query();
|
|
138
|
+
const queryOptions = parseQueryOptions(queryDict);
|
|
139
|
+
const driver = c.get("driver") || this.driver;
|
|
140
|
+
const fetchService = this.getFetchService(driver);
|
|
141
|
+
const hookCtx = this.buildHookContext(c, "GET");
|
|
142
|
+
|
|
143
|
+
// Use include-aware path when available
|
|
144
|
+
if (fetchService) {
|
|
145
|
+
const collectionPath = collection.slug;
|
|
146
|
+
let entity = await fetchService.fetchEntityForRest(
|
|
147
|
+
collectionPath,
|
|
148
|
+
String(id),
|
|
149
|
+
queryOptions.include
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (!entity) {
|
|
153
|
+
throw ApiError.notFound("Entity not found");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
entity = await this.applyAfterRead(collection.slug, entity, hookCtx);
|
|
157
|
+
if (!entity) {
|
|
158
|
+
throw ApiError.notFound("Entity not found");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return c.json(entity);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Fallback
|
|
165
|
+
let entity = await this.fetchRawEntity(driver, resolvedCollection, String(id));
|
|
166
|
+
|
|
167
|
+
if (!entity) {
|
|
168
|
+
throw ApiError.notFound("Entity not found");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
entity = await this.applyAfterRead(collection.slug, entity, hookCtx);
|
|
172
|
+
if (!entity) {
|
|
173
|
+
throw ApiError.notFound("Entity not found");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return c.json(entity);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// POST /collection - Create entity
|
|
180
|
+
this.router.post(basePath, async (c) => {
|
|
181
|
+
try {
|
|
182
|
+
const driver = c.get("driver") || this.driver;
|
|
183
|
+
const path = collection.slug;
|
|
184
|
+
const hookCtx = this.buildHookContext(c, "POST");
|
|
185
|
+
|
|
186
|
+
let body = await c.req.json().catch(() => ({}));
|
|
187
|
+
|
|
188
|
+
if (this.dataHooks?.beforeSave) {
|
|
189
|
+
body = await this.dataHooks.beforeSave(path, body, undefined, hookCtx);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const entity = await driver.saveEntity({
|
|
193
|
+
path,
|
|
194
|
+
values: body,
|
|
195
|
+
collection: resolvedCollection,
|
|
196
|
+
status: "new"
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const response = this.formatResponse(entity);
|
|
200
|
+
|
|
201
|
+
if (this.dataHooks?.afterSave) {
|
|
202
|
+
Promise.resolve(this.dataHooks.afterSave(path, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
203
|
+
console.error("[BackendHooks] data.afterSave error:", err instanceof Error ? err.message : err);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return c.json(response, 201);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const err = error as Error & { code?: string };
|
|
210
|
+
err.code = err.code || "BAD_REQUEST";
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// PUT /collection/:id - Update entity
|
|
216
|
+
this.router.put(`${basePath}/:id`, async (c) => {
|
|
217
|
+
try {
|
|
218
|
+
const id = c.req.param("id");
|
|
219
|
+
const driver = c.get("driver") || this.driver;
|
|
220
|
+
const hookCtx = this.buildHookContext(c, "PUT");
|
|
221
|
+
|
|
222
|
+
const existingEntity = await driver.fetchEntity({
|
|
223
|
+
path: collection.slug,
|
|
224
|
+
entityId: String(id),
|
|
225
|
+
collection: resolvedCollection
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (!existingEntity) {
|
|
229
|
+
throw ApiError.notFound("Entity not found");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let body = await c.req.json().catch(() => ({}));
|
|
233
|
+
|
|
234
|
+
if (this.dataHooks?.beforeSave) {
|
|
235
|
+
body = await this.dataHooks.beforeSave(collection.slug, body, String(id), hookCtx);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const entity = await driver.saveEntity({
|
|
239
|
+
path: collection.slug,
|
|
240
|
+
entityId: String(id),
|
|
241
|
+
values: body,
|
|
242
|
+
collection: resolvedCollection,
|
|
243
|
+
status: "existing"
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const response = this.formatResponse(entity);
|
|
247
|
+
|
|
248
|
+
if (this.dataHooks?.afterSave) {
|
|
249
|
+
Promise.resolve(this.dataHooks.afterSave(collection.slug, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
250
|
+
console.error("[BackendHooks] data.afterSave error:", err instanceof Error ? err.message : err);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return c.json(response);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
const err = error as Error & { code?: string };
|
|
257
|
+
err.code = err.code || "BAD_REQUEST";
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// DELETE /collection/:id - Delete entity
|
|
263
|
+
this.router.delete(`${basePath}/:id`, async (c) => {
|
|
264
|
+
const id = c.req.param("id");
|
|
265
|
+
const driver = c.get("driver") || this.driver;
|
|
266
|
+
const hookCtx = this.buildHookContext(c, "DELETE");
|
|
267
|
+
|
|
268
|
+
const existingEntity = await driver.fetchEntity({
|
|
269
|
+
path: collection.slug,
|
|
270
|
+
entityId: String(id),
|
|
271
|
+
collection: resolvedCollection
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (!existingEntity) {
|
|
275
|
+
throw ApiError.notFound("Entity not found");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (this.dataHooks?.beforeDelete) {
|
|
279
|
+
await this.dataHooks.beforeDelete(collection.slug, String(id), hookCtx);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
await driver.deleteEntity({
|
|
283
|
+
entity: existingEntity,
|
|
284
|
+
collection: resolvedCollection
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
if (this.dataHooks?.afterDelete) {
|
|
288
|
+
Promise.resolve(this.dataHooks.afterDelete(collection.slug, String(id), hookCtx)).catch(err => {
|
|
289
|
+
console.error("[BackendHooks] data.afterDelete error:", err instanceof Error ? err.message : err);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return new Response(null, { status: 204 });
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Catch-all routes for subcollection paths.
|
|
299
|
+
*
|
|
300
|
+
* Matches URL patterns like:
|
|
301
|
+
* GET /authors/111094/posts → list child collection
|
|
302
|
+
* GET /authors/111094/posts/43 → get child entity
|
|
303
|
+
* POST /authors/111094/posts → create child entity
|
|
304
|
+
* PUT /authors/111094/posts/43 → update child entity
|
|
305
|
+
* DELETE /authors/111094/posts/43 → delete child entity
|
|
306
|
+
*
|
|
307
|
+
* The `:rest{.+}` regex param captures the full remainder of the URL
|
|
308
|
+
* path (Hono v4 `*` wildcard does not populate `c.req.param("*")`).
|
|
309
|
+
* We split it into segments and reconstruct the `collectionPath`
|
|
310
|
+
* (e.g. "authors/111094/posts") and optional `entityId` (e.g. "43").
|
|
311
|
+
*
|
|
312
|
+
* The DataDriver.saveEntity / fetchCollection / etc. already know how to
|
|
313
|
+
* resolve multi-segment relation paths, so we just forward to them.
|
|
314
|
+
*/
|
|
315
|
+
private createSubcollectionRoutes(): void {
|
|
316
|
+
// Reserved path segments that should NOT be treated as relation names.
|
|
317
|
+
// These are handled by dedicated route handlers (e.g., history routes)
|
|
318
|
+
// mounted on the same data router.
|
|
319
|
+
const RESERVED_SEGMENTS = new Set(["history"]);
|
|
320
|
+
|
|
321
|
+
// Helper: parse a path like "authors/111094/posts/43" into
|
|
322
|
+
// { collectionPath: "authors/111094/posts", entityId: "43" }
|
|
323
|
+
// or "authors/111094/posts" into
|
|
324
|
+
// { collectionPath: "authors/111094/posts", entityId: undefined }
|
|
325
|
+
const parseSubPath = (rawPath: string): { collectionPath: string; entityId?: string } | null => {
|
|
326
|
+
const segments = rawPath.split("/").filter(s => s && s !== "undefined");
|
|
327
|
+
// Need at least 3 segments for a subcollection path (parent/id/child)
|
|
328
|
+
if (segments.length < 3) return null;
|
|
329
|
+
|
|
330
|
+
// If any segment is a reserved path (e.g. "history"), this is not a
|
|
331
|
+
// subcollection route — let it fall through to other handlers.
|
|
332
|
+
if (segments.some(s => RESERVED_SEGMENTS.has(s))) return null;
|
|
333
|
+
|
|
334
|
+
// Odd segment count → collection path (parent/id/child or parent/id/child/id2/grandchild)
|
|
335
|
+
// Even segment count → entity path (parent/id/child/entityId)
|
|
336
|
+
if (segments.length % 2 === 1) {
|
|
337
|
+
return { collectionPath: segments.join("/") };
|
|
338
|
+
} else {
|
|
339
|
+
const entityId = segments.pop()!;
|
|
340
|
+
return { collectionPath: segments.join("/"),
|
|
341
|
+
entityId };
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// GET /<subcollection-path> — list or get single entity
|
|
346
|
+
// Use :rest{.+} instead of * because Hono v4's wildcard doesn't
|
|
347
|
+
// capture into c.req.param("*") — it always returns undefined.
|
|
348
|
+
this.router.get("/:parent/:parentId/:rest{.+}", async (c, next) => {
|
|
349
|
+
const rest = c.req.param("rest");
|
|
350
|
+
if (!rest || rest === "undefined") return next();
|
|
351
|
+
const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
|
|
352
|
+
const parsed = parseSubPath(rawPath);
|
|
353
|
+
if (!parsed) return next();
|
|
354
|
+
|
|
355
|
+
const driver = c.get("driver") || this.driver;
|
|
356
|
+
|
|
357
|
+
if (parsed.entityId === "count") {
|
|
358
|
+
// GET /parent/:parentId/child/count — count child entities
|
|
359
|
+
const queryDict = c.req.query();
|
|
360
|
+
const queryOptions = parseQueryOptions(queryDict);
|
|
361
|
+
|
|
362
|
+
const total = driver.countEntities ? await driver.countEntities({
|
|
363
|
+
path: parsed.collectionPath,
|
|
364
|
+
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
365
|
+
searchString: queryDict.searchString as string | undefined
|
|
366
|
+
}) : 0;
|
|
367
|
+
|
|
368
|
+
return c.json({ count: total });
|
|
369
|
+
} else if (parsed.entityId) {
|
|
370
|
+
// GET /parent/:parentId/child/:id — single entity
|
|
371
|
+
const entity = await driver.fetchEntity({
|
|
372
|
+
path: parsed.collectionPath,
|
|
373
|
+
entityId: parsed.entityId
|
|
374
|
+
});
|
|
375
|
+
if (!entity) throw ApiError.notFound("Entity not found");
|
|
376
|
+
return c.json(this.flattenEntity(entity));
|
|
377
|
+
} else {
|
|
378
|
+
// GET /parent/:parentId/child — list entities
|
|
379
|
+
const queryDict = c.req.query();
|
|
380
|
+
const queryOptions = parseQueryOptions(queryDict);
|
|
381
|
+
const entities = await driver.fetchCollection({
|
|
382
|
+
path: parsed.collectionPath,
|
|
383
|
+
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
384
|
+
limit: queryOptions.limit,
|
|
385
|
+
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
386
|
+
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
387
|
+
searchString: queryDict.searchString as string | undefined
|
|
388
|
+
});
|
|
389
|
+
return c.json({
|
|
390
|
+
data: entities.map(e => this.flattenEntity(e)),
|
|
391
|
+
meta: {
|
|
392
|
+
total: entities.length,
|
|
393
|
+
limit: queryOptions.limit,
|
|
394
|
+
offset: queryOptions.offset,
|
|
395
|
+
hasMore: false
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// POST /<subcollection-path> — create entity
|
|
402
|
+
this.router.post("/:parent/:parentId/:rest{.+}", async (c, next) => {
|
|
403
|
+
const rest = c.req.param("rest");
|
|
404
|
+
if (!rest || rest === "undefined") return next();
|
|
405
|
+
const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
|
|
406
|
+
const parsed = parseSubPath(rawPath);
|
|
407
|
+
if (!parsed || parsed.entityId) return next();
|
|
408
|
+
|
|
409
|
+
const driver = c.get("driver") || this.driver;
|
|
410
|
+
const body = await c.req.json().catch(() => ({}));
|
|
411
|
+
|
|
412
|
+
const entity = await driver.saveEntity({
|
|
413
|
+
path: parsed.collectionPath,
|
|
414
|
+
values: body,
|
|
415
|
+
status: "new"
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
return c.json(this.formatResponse(entity), 201);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// PUT /<subcollection-path>/:id — update entity
|
|
422
|
+
this.router.put("/:parent/:parentId/:rest{.+}", async (c, next) => {
|
|
423
|
+
const rest = c.req.param("rest");
|
|
424
|
+
if (!rest || rest === "undefined") return next();
|
|
425
|
+
const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
|
|
426
|
+
const parsed = parseSubPath(rawPath);
|
|
427
|
+
if (!parsed || !parsed.entityId) return next();
|
|
428
|
+
|
|
429
|
+
const driver = c.get("driver") || this.driver;
|
|
430
|
+
const body = await c.req.json().catch(() => ({}));
|
|
431
|
+
|
|
432
|
+
const entity = await driver.saveEntity({
|
|
433
|
+
path: parsed.collectionPath,
|
|
434
|
+
entityId: parsed.entityId,
|
|
435
|
+
values: body,
|
|
436
|
+
status: "existing"
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
return c.json(this.formatResponse(entity));
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// DELETE /<subcollection-path>/:id — delete entity
|
|
443
|
+
this.router.delete("/:parent/:parentId/:rest{.+}", async (c, next) => {
|
|
444
|
+
const rest = c.req.param("rest");
|
|
445
|
+
if (!rest || rest === "undefined") return next();
|
|
446
|
+
const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
|
|
447
|
+
const parsed = parseSubPath(rawPath);
|
|
448
|
+
if (!parsed || !parsed.entityId) return next();
|
|
449
|
+
|
|
450
|
+
const driver = c.get("driver") || this.driver;
|
|
451
|
+
|
|
452
|
+
const existingEntity = await driver.fetchEntity({
|
|
453
|
+
path: parsed.collectionPath,
|
|
454
|
+
entityId: parsed.entityId
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
if (!existingEntity) throw ApiError.notFound("Entity not found");
|
|
458
|
+
|
|
459
|
+
await driver.deleteEntity({ entity: existingEntity });
|
|
460
|
+
|
|
461
|
+
return new Response(null, { status: 204 });
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Format successful API response - flattened for traditional REST API
|
|
467
|
+
*/
|
|
468
|
+
private formatResponse<T>(data: T, meta?: Record<string, unknown>): unknown {
|
|
469
|
+
if (Array.isArray(data)) {
|
|
470
|
+
const flattenedData = data.map(entity => this.flattenEntity(entity));
|
|
471
|
+
if (meta) {
|
|
472
|
+
return {
|
|
473
|
+
data: flattenedData,
|
|
474
|
+
meta
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return flattenedData;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (data && typeof data === "object" && "values" in data) {
|
|
481
|
+
return this.flattenEntity(data as unknown as Entity<Record<string, unknown>>);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (meta) {
|
|
485
|
+
return {
|
|
486
|
+
data,
|
|
487
|
+
meta
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return data;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Flatten Rebase entity structure to traditional REST format
|
|
495
|
+
*/
|
|
496
|
+
private flattenEntity(entity: Entity<Record<string, unknown>>): Record<string, unknown> {
|
|
497
|
+
if (!entity || typeof entity !== "object") {
|
|
498
|
+
return entity;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if ("values" in entity && typeof entity.values === "object") {
|
|
502
|
+
return {
|
|
503
|
+
id: entity.id,
|
|
504
|
+
...entity.values
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return entity as unknown as Record<string, unknown>;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Fetch raw collection data without Entity wrapper (fallback for non-Postgres)
|
|
513
|
+
*/
|
|
514
|
+
private async fetchRawCollection(driver: DataDriver, collection: EntityCollection, queryOptions: QueryOptions, searchString?: string) {
|
|
515
|
+
const entities = await driver.fetchCollection({
|
|
516
|
+
path: collection.slug,
|
|
517
|
+
collection,
|
|
518
|
+
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
519
|
+
limit: queryOptions.limit,
|
|
520
|
+
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
521
|
+
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
522
|
+
startAfter: queryOptions.offset ? String(queryOptions.offset) : undefined,
|
|
523
|
+
searchString
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
return entities.map(entity => this.flattenEntity(entity));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Count raw entities for a collection
|
|
531
|
+
*/
|
|
532
|
+
private async countRawEntities(driver: DataDriver, collection: EntityCollection, queryOptions: QueryOptions, searchString?: string): Promise<number> {
|
|
533
|
+
return driver.countEntities ? await driver.countEntities({
|
|
534
|
+
path: collection.slug,
|
|
535
|
+
collection,
|
|
536
|
+
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
537
|
+
searchString
|
|
538
|
+
}) : 0;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Fetch single entity raw data without Entity wrapper (fallback)
|
|
543
|
+
*/
|
|
544
|
+
private async fetchRawEntity(driver: DataDriver, collection: EntityCollection, entityId: string) {
|
|
545
|
+
const entity = await driver.fetchEntity({
|
|
546
|
+
path: collection.slug,
|
|
547
|
+
entityId,
|
|
548
|
+
collection
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
return entity ? this.flattenEntity(entity) : null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Apply data.afterRead hook to a single entity.
|
|
556
|
+
* Returns the transformed entity, or null to filter it out.
|
|
557
|
+
*/
|
|
558
|
+
private async applyAfterRead(slug: string, entity: Record<string, unknown>, ctx: BackendHookContext): Promise<Record<string, unknown> | null> {
|
|
559
|
+
if (!this.dataHooks?.afterRead) return entity;
|
|
560
|
+
return this.dataHooks.afterRead(slug, entity, ctx);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Apply data.afterRead hook to an array of entities, filtering out nulls.
|
|
565
|
+
*/
|
|
566
|
+
private async applyAfterReadBatch(slug: string, entities: Record<string, unknown>[], ctx: BackendHookContext): Promise<Record<string, unknown>[]> {
|
|
567
|
+
if (!this.dataHooks?.afterRead) return entities;
|
|
568
|
+
const results = await Promise.all(
|
|
569
|
+
entities.map(e => this.applyAfterRead(slug, e, ctx))
|
|
570
|
+
);
|
|
571
|
+
return results.filter((e): e is Record<string, unknown> => e !== null);
|
|
572
|
+
}
|
|
573
|
+
}
|