@rebasepro/server-core 0.0.1-canary.4d4fb3e
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 +48 -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 +36 -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 +12 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index-BeMqpmfQ.js +239 -0
- package/dist/index-BeMqpmfQ.js.map +1 -0
- package/dist/index-bl4J3lNb.js +55823 -0
- package/dist/index-bl4J3lNb.js.map +1 -0
- package/dist/index.es.js +58 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +56062 -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 +2 -0
- package/dist/server-core/src/api/rest/api-generator.d.ts +64 -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 +7 -0
- package/dist/server-core/src/auth/google-oauth.d.ts +20 -0
- package/dist/server-core/src/auth/index.d.ts +12 -0
- package/dist/server-core/src/auth/interfaces.d.ts +270 -0
- package/dist/server-core/src/auth/jwt.d.ts +42 -0
- package/dist/server-core/src/auth/middleware.d.ts +56 -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 +17 -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/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 +33 -0
- package/dist/server-core/src/email/types.d.ts +110 -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 +24 -0
- package/dist/server-core/src/init.d.ts +49 -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/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 +18 -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 +91 -0
- package/dist/server-core/src/types/index.d.ts +11 -0
- package/dist/server-core/src/utils/logging.d.ts +9 -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 +117 -0
- package/dist/types/src/controllers/client.d.ts +58 -0
- package/dist/types/src/controllers/collection_registry.d.ts +44 -0
- package/dist/types/src/controllers/customization_controller.d.ts +54 -0
- package/dist/types/src/controllers/data.d.ts +141 -0
- package/dist/types/src/controllers/data_driver.d.ts +168 -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/index.d.ts +17 -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 +51 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +173 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +101 -0
- package/dist/types/src/types/backend.d.ts +533 -0
- package/dist/types/src/types/builders.d.ts +14 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +812 -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 +9 -0
- package/dist/types/src/types/entity_views.d.ts +61 -0
- package/dist/types/src/types/export_import.d.ts +21 -0
- package/dist/types/src/types/index.d.ts +22 -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 +225 -0
- package/dist/types/src/types/properties.d.ts +1091 -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 +228 -0
- package/dist/types/src/types/translations.d.ts +826 -0
- package/dist/types/src/types/user_management_delegate.d.ts +120 -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 +8 -0
- package/src/api/ast-schema-editor.ts +289 -0
- package/src/api/collections_for_test/callbacks_test_collection.ts +57 -0
- package/src/api/errors.ts +155 -0
- package/src/api/graphql/graphql-schema-generator.ts +334 -0
- package/src/api/graphql/index.ts +2 -0
- package/src/api/index.ts +11 -0
- package/src/api/openapi-generator.ts +160 -0
- package/src/api/rest/api-generator.ts +466 -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 +39 -0
- package/src/api/server.ts +245 -0
- package/src/api/types.ts +90 -0
- package/src/auth/admin-routes.ts +488 -0
- package/src/auth/google-oauth.ts +60 -0
- package/src/auth/index.ts +21 -0
- package/src/auth/interfaces.ts +316 -0
- package/src/auth/jwt.ts +164 -0
- package/src/auth/middleware.ts +235 -0
- package/src/auth/password.ts +75 -0
- package/src/auth/rate-limiter.ts +129 -0
- package/src/auth/routes.ts +730 -0
- package/src/bootstrappers/index.ts +1 -0
- package/src/collections/BackendCollectionRegistry.ts +20 -0
- package/src/collections/loader.ts +49 -0
- package/src/db/interfaces.ts +60 -0
- package/src/email/index.ts +17 -0
- package/src/email/smtp-email-service.ts +88 -0
- package/src/email/templates.ts +301 -0
- package/src/email/types.ts +112 -0
- package/src/functions/function-loader.ts +91 -0
- package/src/functions/function-routes.ts +31 -0
- package/src/functions/index.ts +3 -0
- package/src/history/history-routes.ts +128 -0
- package/src/history/index.ts +2 -0
- package/src/index.ts +56 -0
- package/src/init.ts +309 -0
- package/src/serve-spa.ts +81 -0
- package/src/services/driver-registry.ts +182 -0
- package/src/storage/LocalStorageController.ts +368 -0
- package/src/storage/S3StorageController.ts +295 -0
- package/src/storage/index.ts +32 -0
- package/src/storage/routes.ts +247 -0
- package/src/storage/storage-registry.ts +187 -0
- package/src/storage/types.ts +122 -0
- package/src/types/index.ts +27 -0
- package/src/utils/logging.ts +35 -0
- package/src/utils/sql.ts +38 -0
- package/test/admin-routes.test.ts +591 -0
- package/test/api-generator.test.ts +458 -0
- package/test/ast-schema-editor.test.ts +61 -0
- package/test/auth-middleware-hono.test.ts +321 -0
- package/test/auth-routes.test.ts +868 -0
- package/test/driver-registry.test.ts +280 -0
- package/test/errors-hono.test.ts +133 -0
- package/test/errors.test.ts +150 -0
- package/test/jwt-security.test.ts +173 -0
- package/test/jwt.test.ts +311 -0
- package/test/middleware.test.ts +295 -0
- package/test/password.test.ts +165 -0
- package/test/query-parser.test.ts +258 -0
- package/test/rate-limiter.test.ts +102 -0
- package/test/storage-local.test.ts +278 -0
- package/test/storage-registry.test.ts +280 -0
- package/test/storage-routes.test.ts +218 -0
- package/test/storage-s3.test.ts +301 -0
- package/test-ast.ts +28 -0
- package/test_output.txt +1133 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- package/vite.config.ts +78 -0
- package/vite.config.ts.timestamp-1775065397568-8a853255edf6e.mjs +46 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { RestApiGenerator } from "../src/api/rest/api-generator.js";
|
|
3
|
+
import { errorHandler } from "../src/api/errors.js";
|
|
4
|
+
import { DataDriver } from "../../types/src/controllers/data_driver.js";
|
|
5
|
+
import { EntityCollection } from "../../types/src/types/collections.js";
|
|
6
|
+
|
|
7
|
+
describe("RestApiGenerator", () => {
|
|
8
|
+
let mockDriver: jest.Mocked<DataDriver>;
|
|
9
|
+
let mockCollections: EntityCollection[];
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockDriver = {
|
|
13
|
+
key: "postgres",
|
|
14
|
+
initialised: true,
|
|
15
|
+
fetchCollection: jest.fn(),
|
|
16
|
+
listenCollection: jest.fn(),
|
|
17
|
+
fetchEntity: jest.fn(),
|
|
18
|
+
listenEntity: jest.fn(),
|
|
19
|
+
saveEntity: jest.fn(),
|
|
20
|
+
deleteEntity: jest.fn(),
|
|
21
|
+
checkUniqueField: jest.fn(),
|
|
22
|
+
countEntities: jest.fn(),
|
|
23
|
+
withAuth: jest.fn(),
|
|
24
|
+
admin: {} as any
|
|
25
|
+
} as unknown as jest.Mocked<DataDriver>;
|
|
26
|
+
|
|
27
|
+
mockCollections = [
|
|
28
|
+
{
|
|
29
|
+
slug: "users",
|
|
30
|
+
name: "Users",
|
|
31
|
+
singularName: "User",
|
|
32
|
+
properties: {}
|
|
33
|
+
} as any,
|
|
34
|
+
{
|
|
35
|
+
slug: "posts",
|
|
36
|
+
name: "Posts",
|
|
37
|
+
singularName: "Post",
|
|
38
|
+
properties: {}
|
|
39
|
+
} as any
|
|
40
|
+
];
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Mount with "/api" prefix — mirrors production routing in init.ts.
|
|
45
|
+
*/
|
|
46
|
+
function createApp() {
|
|
47
|
+
const app = new Hono();
|
|
48
|
+
app.onError(errorHandler);
|
|
49
|
+
const generator = new RestApiGenerator(mockCollections, mockDriver);
|
|
50
|
+
app.route("/api", generator.generateRoutes());
|
|
51
|
+
return app;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Mount directly at root — eliminates Hono sub-router prefix stripping
|
|
56
|
+
* so we can reliably test route matching without nesting interference.
|
|
57
|
+
*/
|
|
58
|
+
function createFlatApp() {
|
|
59
|
+
const generator = new RestApiGenerator(mockCollections, mockDriver);
|
|
60
|
+
const app = generator.generateRoutes();
|
|
61
|
+
app.onError(errorHandler);
|
|
62
|
+
return app;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("Core Collection Routes", () => {
|
|
66
|
+
it("list entities - GET /api/users", async () => {
|
|
67
|
+
const app = createApp();
|
|
68
|
+
mockDriver.fetchCollection.mockResolvedValue([
|
|
69
|
+
{ id: "1", path: "users", values: { name: "Alice" } } as any
|
|
70
|
+
]);
|
|
71
|
+
mockDriver.countEntities!.mockResolvedValue(1);
|
|
72
|
+
|
|
73
|
+
const res = await app.request("/api/users?limit=10");
|
|
74
|
+
expect(res.status).toBe(200);
|
|
75
|
+
|
|
76
|
+
const body = await res.json() as any;
|
|
77
|
+
expect(body.data).toEqual([{ id: "1", name: "Alice" }]);
|
|
78
|
+
expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
|
|
79
|
+
expect.objectContaining({ path: "users", limit: 10 })
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("get entity - GET /api/users/123", async () => {
|
|
84
|
+
const app = createApp();
|
|
85
|
+
mockDriver.fetchEntity.mockResolvedValue(
|
|
86
|
+
{ id: "123", path: "users", values: { name: "Alice" } } as any
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const res = await app.request("/api/users/123");
|
|
90
|
+
expect(res.status).toBe(200);
|
|
91
|
+
|
|
92
|
+
const body = await res.json() as any;
|
|
93
|
+
expect(body.id).toBe("123");
|
|
94
|
+
expect(body.name).toBe("Alice");
|
|
95
|
+
expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
|
|
96
|
+
expect.objectContaining({ path: "users", entityId: "123" })
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("create entity - POST /api/users", async () => {
|
|
101
|
+
const app = createApp();
|
|
102
|
+
mockDriver.saveEntity.mockResolvedValue(
|
|
103
|
+
{ id: "new-1", path: "users", values: { name: "Bob" } } as any
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const res = await app.request("/api/users", {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { "Content-Type": "application/json" },
|
|
109
|
+
body: JSON.stringify({ name: "Bob" })
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(res.status).toBe(201);
|
|
113
|
+
expect(mockDriver.saveEntity).toHaveBeenCalledWith(
|
|
114
|
+
expect.objectContaining({ path: "users", values: { name: "Bob" }, status: "new" })
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("update entity - PUT /api/users/123", async () => {
|
|
119
|
+
const app = createApp();
|
|
120
|
+
mockDriver.fetchEntity.mockResolvedValue({ id: "123", path: "users", values: {} } as any);
|
|
121
|
+
mockDriver.saveEntity.mockResolvedValue(
|
|
122
|
+
{ id: "123", path: "users", values: { name: "Bob Jr" } } as any
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const res = await app.request("/api/users/123", {
|
|
126
|
+
method: "PUT",
|
|
127
|
+
headers: { "Content-Type": "application/json" },
|
|
128
|
+
body: JSON.stringify({ name: "Bob Jr" })
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(res.status).toBe(200);
|
|
132
|
+
expect(mockDriver.saveEntity).toHaveBeenCalledWith(
|
|
133
|
+
expect.objectContaining({ path: "users", entityId: "123", values: { name: "Bob Jr" }, status: "existing" })
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("delete entity - DELETE /api/users/123", async () => {
|
|
138
|
+
const app = createApp();
|
|
139
|
+
const existingEntity = { id: "123", path: "users", values: {} } as any;
|
|
140
|
+
mockDriver.fetchEntity.mockResolvedValue(existingEntity);
|
|
141
|
+
mockDriver.deleteEntity.mockResolvedValue();
|
|
142
|
+
|
|
143
|
+
const res = await app.request("/api/users/123", { method: "DELETE" });
|
|
144
|
+
|
|
145
|
+
expect(res.status).toBe(204);
|
|
146
|
+
expect(mockDriver.deleteEntity).toHaveBeenCalledWith(
|
|
147
|
+
expect.objectContaining({ entity: existingEntity })
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("Subcollection Routes", () => {
|
|
153
|
+
/**
|
|
154
|
+
* These tests use the flat app (no prefix nesting) to avoid
|
|
155
|
+
* Hono sub-router routing quirks where `/:slug/:id` eats the request
|
|
156
|
+
* before the `/:parent/:parentId/*` wildcard gets a chance.
|
|
157
|
+
*
|
|
158
|
+
* We use "authors" as the parent slug since it's NOT a registered
|
|
159
|
+
* collection, so it can only match the wildcard catch-all.
|
|
160
|
+
*/
|
|
161
|
+
|
|
162
|
+
it("list subcollection - GET /authors/123/posts", async () => {
|
|
163
|
+
const app = createFlatApp();
|
|
164
|
+
mockDriver.fetchCollection.mockResolvedValue([
|
|
165
|
+
{ id: "post-1", path: "authors/123/posts", values: { title: "Hello" } } as any
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
const res = await app.request("/authors/123/posts");
|
|
169
|
+
expect(res.status).toBe(200);
|
|
170
|
+
|
|
171
|
+
const body = await res.json() as any;
|
|
172
|
+
expect(body.data).toEqual([{ id: "post-1", title: "Hello" }]);
|
|
173
|
+
expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
|
|
174
|
+
expect.objectContaining({ path: "authors/123/posts" })
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("get subcollection entity - GET /authors/123/posts/456", async () => {
|
|
179
|
+
const app = createFlatApp();
|
|
180
|
+
mockDriver.fetchEntity.mockResolvedValue(
|
|
181
|
+
{ id: "456", path: "authors/123/posts", values: { title: "Hello" } } as any
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const res = await app.request("/authors/123/posts/456");
|
|
185
|
+
expect(res.status).toBe(200);
|
|
186
|
+
|
|
187
|
+
const body = await res.json() as any;
|
|
188
|
+
expect(body.id).toBe("456");
|
|
189
|
+
expect(body.title).toBe("Hello");
|
|
190
|
+
expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
|
|
191
|
+
expect.objectContaining({ path: "authors/123/posts", entityId: "456" })
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("create subcollection entity - POST /authors/123/posts", async () => {
|
|
196
|
+
const app = createFlatApp();
|
|
197
|
+
mockDriver.saveEntity.mockResolvedValue(
|
|
198
|
+
{ id: "new-post", path: "authors/123/posts", values: { title: "New" } } as any
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const res = await app.request("/authors/123/posts", {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: { "Content-Type": "application/json" },
|
|
204
|
+
body: JSON.stringify({ title: "New" })
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(res.status).toBe(201);
|
|
208
|
+
expect(mockDriver.saveEntity).toHaveBeenCalledWith(
|
|
209
|
+
expect.objectContaining({ path: "authors/123/posts", status: "new" })
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("update subcollection entity - PUT /authors/123/posts/456", async () => {
|
|
214
|
+
const app = createFlatApp();
|
|
215
|
+
mockDriver.saveEntity.mockResolvedValue(
|
|
216
|
+
{ id: "456", path: "authors/123/posts", values: { title: "Updated" } } as any
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const res = await app.request("/authors/123/posts/456", {
|
|
220
|
+
method: "PUT",
|
|
221
|
+
headers: { "Content-Type": "application/json" },
|
|
222
|
+
body: JSON.stringify({ title: "Updated" })
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(res.status).toBe(200);
|
|
226
|
+
expect(mockDriver.saveEntity).toHaveBeenCalledWith(
|
|
227
|
+
expect.objectContaining({
|
|
228
|
+
path: "authors/123/posts",
|
|
229
|
+
entityId: "456",
|
|
230
|
+
status: "existing"
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("delete subcollection entity - DELETE /authors/123/posts/456", async () => {
|
|
236
|
+
const app = createFlatApp();
|
|
237
|
+
const existingEntity = { id: "456", path: "authors/123/posts", values: {} } as any;
|
|
238
|
+
mockDriver.fetchEntity.mockResolvedValue(existingEntity);
|
|
239
|
+
mockDriver.deleteEntity.mockResolvedValue();
|
|
240
|
+
|
|
241
|
+
const res = await app.request("/authors/123/posts/456", { method: "DELETE" });
|
|
242
|
+
|
|
243
|
+
expect(res.status).toBe(204);
|
|
244
|
+
expect(mockDriver.deleteEntity).toHaveBeenCalledWith(
|
|
245
|
+
expect.objectContaining({ entity: existingEntity })
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("deeply nested subcollection list - GET /authors/123/posts/456/comments", async () => {
|
|
250
|
+
const app = createFlatApp();
|
|
251
|
+
mockDriver.fetchCollection.mockResolvedValue([
|
|
252
|
+
{ id: "c-1", path: "authors/123/posts/456/comments", values: { text: "Wow" } } as any
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
const res = await app.request("/authors/123/posts/456/comments");
|
|
256
|
+
expect(res.status).toBe(200);
|
|
257
|
+
|
|
258
|
+
const body = await res.json() as any;
|
|
259
|
+
expect(body.data).toEqual([{ id: "c-1", text: "Wow" }]);
|
|
260
|
+
expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
|
|
261
|
+
expect.objectContaining({ path: "authors/123/posts/456/comments" })
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("deeply nested entity - GET /authors/123/posts/456/comments/789", async () => {
|
|
266
|
+
const app = createFlatApp();
|
|
267
|
+
mockDriver.fetchEntity.mockResolvedValue(
|
|
268
|
+
{ id: "789", path: "authors/123/posts/456/comments", values: { text: "Wow" } } as any
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const res = await app.request("/authors/123/posts/456/comments/789");
|
|
272
|
+
expect(res.status).toBe(200);
|
|
273
|
+
|
|
274
|
+
const body = await res.json() as any;
|
|
275
|
+
expect(body.id).toBe("789");
|
|
276
|
+
expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
|
|
277
|
+
expect.objectContaining({
|
|
278
|
+
path: "authors/123/posts/456/comments",
|
|
279
|
+
entityId: "789"
|
|
280
|
+
})
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("passes query options when listing subcollection", async () => {
|
|
285
|
+
const app = createFlatApp();
|
|
286
|
+
mockDriver.fetchCollection.mockResolvedValue([]);
|
|
287
|
+
|
|
288
|
+
const res = await app.request("/authors/123/posts?limit=5&orderBy=title:desc&searchString=hello");
|
|
289
|
+
expect(res.status).toBe(200);
|
|
290
|
+
|
|
291
|
+
expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
|
|
292
|
+
expect.objectContaining({
|
|
293
|
+
path: "authors/123/posts",
|
|
294
|
+
limit: 5,
|
|
295
|
+
searchString: "hello"
|
|
296
|
+
})
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("Subcollection Wildcard Guard (Undefined / Empty Paths)", () => {
|
|
302
|
+
/**
|
|
303
|
+
* Regression: c.req.param("*") can return "undefined" (literal string)
|
|
304
|
+
* when template literal stringifies JS undefined. This caused:
|
|
305
|
+
* Error: Relation 'undefined' not found in collection 'authors'
|
|
306
|
+
*/
|
|
307
|
+
|
|
308
|
+
it("GET /authors/123/undefined → 404, no driver calls", async () => {
|
|
309
|
+
const app = createFlatApp();
|
|
310
|
+
const res = await app.request("/authors/123/undefined");
|
|
311
|
+
expect(res.status).toBe(404);
|
|
312
|
+
expect(mockDriver.fetchCollection).not.toHaveBeenCalled();
|
|
313
|
+
expect(mockDriver.fetchEntity).not.toHaveBeenCalled();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("POST /authors/123/undefined → 404, no driver calls", async () => {
|
|
317
|
+
const app = createFlatApp();
|
|
318
|
+
const res = await app.request("/authors/123/undefined", {
|
|
319
|
+
method: "POST",
|
|
320
|
+
headers: { "Content-Type": "application/json" },
|
|
321
|
+
body: JSON.stringify({ title: "test" })
|
|
322
|
+
});
|
|
323
|
+
expect(res.status).toBe(404);
|
|
324
|
+
expect(mockDriver.saveEntity).not.toHaveBeenCalled();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("PUT /authors/123/undefined → 404, no driver calls", async () => {
|
|
328
|
+
const app = createFlatApp();
|
|
329
|
+
const res = await app.request("/authors/123/undefined", {
|
|
330
|
+
method: "PUT",
|
|
331
|
+
headers: { "Content-Type": "application/json" },
|
|
332
|
+
body: JSON.stringify({ title: "test" })
|
|
333
|
+
});
|
|
334
|
+
expect(res.status).toBe(404);
|
|
335
|
+
expect(mockDriver.saveEntity).not.toHaveBeenCalled();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("DELETE /authors/123/undefined → 404, no driver calls", async () => {
|
|
339
|
+
const app = createFlatApp();
|
|
340
|
+
const res = await app.request("/authors/123/undefined", { method: "DELETE" });
|
|
341
|
+
expect(res.status).toBe(404);
|
|
342
|
+
expect(mockDriver.deleteEntity).not.toHaveBeenCalled();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("nested undefined segment is filtered: GET /authors/123/undefined/posts → 404 (not enough valid segments)", async () => {
|
|
346
|
+
// After filtering "undefined", segments = ["authors", "123", "posts"] → 3 segments → odd → collection path
|
|
347
|
+
// But the wildcard is "undefined/posts", which is NOT the literal "undefined", so the guard won't catch it.
|
|
348
|
+
// However, parseSubPath filters "undefined" from segments: ["authors", "123", "posts"] → valid 3-segment path.
|
|
349
|
+
const app = createFlatApp();
|
|
350
|
+
mockDriver.fetchCollection.mockResolvedValue([]);
|
|
351
|
+
const res = await app.request("/authors/123/undefined/posts");
|
|
352
|
+
// This should either 200 (if parseSubPath accepts "authors/123/posts") or 404
|
|
353
|
+
// Since wildcard = "undefined/posts" which is truthy and not === "undefined", the guard lets it through.
|
|
354
|
+
// parseSubPath filters "undefined" segments → ["authors", "123", "posts"] → 3 segments → collection path.
|
|
355
|
+
expect(res.status).toBe(200);
|
|
356
|
+
expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
|
|
357
|
+
expect.objectContaining({ path: "authors/123/posts" })
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("returns 404 for bare parent without wildcard - GET /xyz", async () => {
|
|
362
|
+
const app = createFlatApp();
|
|
363
|
+
const res = await app.request("/xyz");
|
|
364
|
+
expect(res.status).toBe(404);
|
|
365
|
+
expect(mockDriver.fetchCollection).not.toHaveBeenCalled();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("returns 404 for only two segments (no relation) - GET /xyz/123", async () => {
|
|
369
|
+
// Two segments: would be handled by /:slug/:id route, but "xyz" isn't a registered collection
|
|
370
|
+
// so the /:slug route won't match. /:parent/:parentId/* won't match either (no wildcard part).
|
|
371
|
+
const app = createFlatApp();
|
|
372
|
+
const res = await app.request("/xyz/123");
|
|
373
|
+
expect(res.status).toBe(404);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe("Path Parsing Edge Cases via HTTP", () => {
|
|
378
|
+
it("numeric IDs are correctly parsed as entityId", async () => {
|
|
379
|
+
const app = createFlatApp();
|
|
380
|
+
mockDriver.fetchEntity.mockResolvedValue(
|
|
381
|
+
{ id: "999", path: "authors/42/books", values: { title: "Test" } } as any
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const res = await app.request("/authors/42/books/999");
|
|
385
|
+
expect(res.status).toBe(200);
|
|
386
|
+
expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
|
|
387
|
+
expect.objectContaining({ path: "authors/42/books", entityId: "999" })
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("UUID-style IDs work as parentId and entityId", async () => {
|
|
392
|
+
const app = createFlatApp();
|
|
393
|
+
const uuid1 = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
|
|
394
|
+
const uuid2 = "ffffffff-ffff-ffff-ffff-ffffffffffff";
|
|
395
|
+
mockDriver.fetchEntity.mockResolvedValue(
|
|
396
|
+
{ id: uuid2, path: `authors/${uuid1}/posts`, values: { title: "UUID" } } as any
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const res = await app.request(`/authors/${uuid1}/posts/${uuid2}`);
|
|
400
|
+
expect(res.status).toBe(200);
|
|
401
|
+
expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
|
|
402
|
+
expect.objectContaining({
|
|
403
|
+
path: `authors/${uuid1}/posts`,
|
|
404
|
+
entityId: uuid2
|
|
405
|
+
})
|
|
406
|
+
);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("entity not found returns 404 for subcollection GET", async () => {
|
|
410
|
+
const app = createFlatApp();
|
|
411
|
+
mockDriver.fetchEntity.mockResolvedValue(undefined);
|
|
412
|
+
|
|
413
|
+
const res = await app.request("/authors/123/posts/999");
|
|
414
|
+
expect(res.status).toBe(404);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("POST to even-segment path returns 404 (entityId present, can't create)", async () => {
|
|
418
|
+
const app = createFlatApp();
|
|
419
|
+
// 4 segments: authors/123/posts/456 — parseSubPath gives entityId="456"
|
|
420
|
+
// POST handler checks: if (parsed.entityId) return c.notFound();
|
|
421
|
+
const res = await app.request("/authors/123/posts/456", {
|
|
422
|
+
method: "POST",
|
|
423
|
+
headers: { "Content-Type": "application/json" },
|
|
424
|
+
body: JSON.stringify({ title: "nope" })
|
|
425
|
+
});
|
|
426
|
+
expect(res.status).toBe(404);
|
|
427
|
+
expect(mockDriver.saveEntity).not.toHaveBeenCalled();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("PUT to odd-segment path returns 404 (no entityId, can't update)", async () => {
|
|
431
|
+
const app = createFlatApp();
|
|
432
|
+
// 3 segments: authors/123/posts — no entityId
|
|
433
|
+
// PUT handler checks: if (!parsed.entityId) return c.notFound();
|
|
434
|
+
const res = await app.request("/authors/123/posts", {
|
|
435
|
+
method: "PUT",
|
|
436
|
+
headers: { "Content-Type": "application/json" },
|
|
437
|
+
body: JSON.stringify({ title: "nope" })
|
|
438
|
+
});
|
|
439
|
+
expect(res.status).toBe(404);
|
|
440
|
+
expect(mockDriver.saveEntity).not.toHaveBeenCalled();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("DELETE to odd-segment path returns 404 (no entityId)", async () => {
|
|
444
|
+
const app = createFlatApp();
|
|
445
|
+
const res = await app.request("/authors/123/posts", { method: "DELETE" });
|
|
446
|
+
expect(res.status).toBe(404);
|
|
447
|
+
expect(mockDriver.deleteEntity).not.toHaveBeenCalled();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("DELETE to non-existent entity returns 404", async () => {
|
|
451
|
+
const app = createFlatApp();
|
|
452
|
+
mockDriver.fetchEntity.mockResolvedValue(undefined);
|
|
453
|
+
const res = await app.request("/authors/123/posts/456", { method: "DELETE" });
|
|
454
|
+
expect(res.status).toBe(404);
|
|
455
|
+
expect(mockDriver.deleteEntity).not.toHaveBeenCalled();
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AstSchemaEditor } from "../src/api/ast-schema-editor";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
|
|
6
|
+
describe("AstSchemaEditor", () => {
|
|
7
|
+
let testDir: string;
|
|
8
|
+
let editor: AstSchemaEditor;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), "ast-schema-editor-test-"));
|
|
12
|
+
editor = new AstSchemaEditor(testDir);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should merge properties while preserving existing functions when saving a collection", async () => {
|
|
20
|
+
// Setup initial file
|
|
21
|
+
const fileContent = `import { EntityCollection } from "@rebasepro/types";
|
|
22
|
+
|
|
23
|
+
const productsCollection: EntityCollection = {
|
|
24
|
+
name: "Products",
|
|
25
|
+
slug: "products",
|
|
26
|
+
properties: {
|
|
27
|
+
id: { type: "string" },
|
|
28
|
+
name: { type: "string" },
|
|
29
|
+
category: {
|
|
30
|
+
type: "reference",
|
|
31
|
+
target: () => categoriesCollection
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default productsCollection;
|
|
37
|
+
`;
|
|
38
|
+
fs.writeFileSync(path.join(testDir, "products.ts"), fileContent);
|
|
39
|
+
|
|
40
|
+
// We want to update the collection data, simulating what the frontend sends
|
|
41
|
+
const updatedData = {
|
|
42
|
+
name: "Updated Products",
|
|
43
|
+
slug: "products",
|
|
44
|
+
properties: {
|
|
45
|
+
id: { type: "string" },
|
|
46
|
+
name: { type: "string", description: "Product name" },
|
|
47
|
+
category: {
|
|
48
|
+
type: "reference"
|
|
49
|
+
// Notice target is dropped since the frontend REST payload wouldn't have it serialized
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
await editor.saveCollection("products", updatedData);
|
|
55
|
+
|
|
56
|
+
const newContent = fs.readFileSync(path.join(testDir, "products.ts"), "utf-8");
|
|
57
|
+
expect(newContent).toContain('name: "Updated Products"');
|
|
58
|
+
expect(newContent).toContain('target: () => categoriesCollection');
|
|
59
|
+
expect(newContent).toContain('description: "Product name"');
|
|
60
|
+
});
|
|
61
|
+
});
|