@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,466 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { DataDriver, Entity, EntityCollection, FetchCollectionProps } 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
|
+
|
|
17
|
+
constructor(collections: EntityCollection[], driver: DataDriver) {
|
|
18
|
+
this.collections = collections;
|
|
19
|
+
this.driver = driver;
|
|
20
|
+
this.router = new Hono<HonoEnv>();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate REST routes using existing DataDriver
|
|
25
|
+
*/
|
|
26
|
+
generateRoutes(): Hono<HonoEnv> {
|
|
27
|
+
this.collections.forEach(collection => {
|
|
28
|
+
this.createCollectionRoutes(collection);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Catch-all routes for subcollection paths like
|
|
32
|
+
// /authors/111094/posts and /authors/111094/posts/43
|
|
33
|
+
// The DataDriver already knows how to resolve nested relation paths.
|
|
34
|
+
this.createSubcollectionRoutes();
|
|
35
|
+
|
|
36
|
+
return this.router;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the EntityFetchService from a driver if it exposes one (for include support)
|
|
41
|
+
*/
|
|
42
|
+
private getFetchService(driver: DataDriver): Record<string, (...args: unknown[]) => unknown> | null {
|
|
43
|
+
if ("entityService" in driver && typeof driver.entityService === "object" && driver.entityService) {
|
|
44
|
+
const es = driver.entityService as Record<string, unknown>;
|
|
45
|
+
if (typeof es.getFetchService === "function") {
|
|
46
|
+
return es.getFetchService();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create REST routes for a collection using existing Rebase patterns
|
|
54
|
+
*/
|
|
55
|
+
private createCollectionRoutes(collection: EntityCollection): void {
|
|
56
|
+
const basePath = `/${collection.slug}`;
|
|
57
|
+
const resolvedCollection = collection;
|
|
58
|
+
|
|
59
|
+
// GET /collection - List entities
|
|
60
|
+
this.router.get(basePath, async (c) => {
|
|
61
|
+
const queryDict = c.req.query();
|
|
62
|
+
const queryOptions = parseQueryOptions(queryDict);
|
|
63
|
+
|
|
64
|
+
const driver = c.get("driver") || this.driver;
|
|
65
|
+
const fetchService = this.getFetchService(driver);
|
|
66
|
+
|
|
67
|
+
// Use include-aware path when available
|
|
68
|
+
if (fetchService) {
|
|
69
|
+
const collectionPath = collection.slug;
|
|
70
|
+
const entities = await fetchService.fetchCollectionForRest(
|
|
71
|
+
collectionPath,
|
|
72
|
+
{
|
|
73
|
+
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
74
|
+
limit: queryOptions.limit,
|
|
75
|
+
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
76
|
+
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
77
|
+
searchString: queryDict.searchString as string | undefined,
|
|
78
|
+
},
|
|
79
|
+
queryOptions.include
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions);
|
|
83
|
+
|
|
84
|
+
return c.json({
|
|
85
|
+
data: entities,
|
|
86
|
+
meta: {
|
|
87
|
+
total,
|
|
88
|
+
limit: queryOptions.limit,
|
|
89
|
+
offset: queryOptions.offset,
|
|
90
|
+
hasMore: (queryOptions.offset || 0) + (entities as unknown[]).length < total
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Fallback for non-Postgres drivers
|
|
96
|
+
const entities = await this.fetchRawCollection(driver, resolvedCollection, queryOptions);
|
|
97
|
+
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions);
|
|
98
|
+
|
|
99
|
+
return c.json({
|
|
100
|
+
data: entities,
|
|
101
|
+
meta: {
|
|
102
|
+
total,
|
|
103
|
+
limit: queryOptions.limit,
|
|
104
|
+
offset: queryOptions.offset,
|
|
105
|
+
hasMore: (queryOptions.offset || 0) + (entities as unknown[]).length < total
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// GET /collection/:id - Get single entity
|
|
111
|
+
this.router.get(`${basePath}/:id`, async (c) => {
|
|
112
|
+
const id = c.req.param("id");
|
|
113
|
+
const queryDict = c.req.query();
|
|
114
|
+
const queryOptions = parseQueryOptions(queryDict);
|
|
115
|
+
const driver = c.get("driver") || this.driver;
|
|
116
|
+
const fetchService = this.getFetchService(driver);
|
|
117
|
+
|
|
118
|
+
// Use include-aware path when available
|
|
119
|
+
if (fetchService) {
|
|
120
|
+
const collectionPath = collection.slug;
|
|
121
|
+
const entity = await fetchService.fetchEntityForRest(
|
|
122
|
+
collectionPath,
|
|
123
|
+
String(id),
|
|
124
|
+
queryOptions.include
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (!entity) {
|
|
128
|
+
throw ApiError.notFound("Entity not found");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return c.json(entity);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Fallback
|
|
135
|
+
const entity = await this.fetchRawEntity(driver, resolvedCollection, String(id));
|
|
136
|
+
|
|
137
|
+
if (!entity) {
|
|
138
|
+
throw ApiError.notFound("Entity not found");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return c.json(entity);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// POST /collection - Create entity
|
|
145
|
+
this.router.post(basePath, async (c) => {
|
|
146
|
+
try {
|
|
147
|
+
const driver = c.get("driver") || this.driver;
|
|
148
|
+
const path = collection.slug;
|
|
149
|
+
|
|
150
|
+
const body = await c.req.json().catch(() => ({}));
|
|
151
|
+
|
|
152
|
+
const entity = await driver.saveEntity({
|
|
153
|
+
path,
|
|
154
|
+
values: body,
|
|
155
|
+
collection: resolvedCollection,
|
|
156
|
+
status: "new"
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return c.json(this.formatResponse(entity), 201);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const err = error as Error & { code?: string };
|
|
162
|
+
err.code = err.code || "BAD_REQUEST";
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// PUT /collection/:id - Update entity
|
|
168
|
+
this.router.put(`${basePath}/:id`, async (c) => {
|
|
169
|
+
try {
|
|
170
|
+
const id = c.req.param("id");
|
|
171
|
+
const driver = c.get("driver") || this.driver;
|
|
172
|
+
|
|
173
|
+
const existingEntity = await driver.fetchEntity({
|
|
174
|
+
path: collection.slug,
|
|
175
|
+
entityId: String(id),
|
|
176
|
+
collection: resolvedCollection
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!existingEntity) {
|
|
180
|
+
throw ApiError.notFound("Entity not found");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const body = await c.req.json().catch(() => ({}));
|
|
184
|
+
|
|
185
|
+
const entity = await driver.saveEntity({
|
|
186
|
+
path: collection.slug,
|
|
187
|
+
entityId: String(id),
|
|
188
|
+
values: body,
|
|
189
|
+
collection: resolvedCollection,
|
|
190
|
+
status: "existing"
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return c.json(this.formatResponse(entity));
|
|
194
|
+
} catch (error) {
|
|
195
|
+
const err = error as Error & { code?: string };
|
|
196
|
+
err.code = err.code || "BAD_REQUEST";
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// DELETE /collection/:id - Delete entity
|
|
202
|
+
this.router.delete(`${basePath}/:id`, async (c) => {
|
|
203
|
+
const id = c.req.param("id");
|
|
204
|
+
const driver = c.get("driver") || this.driver;
|
|
205
|
+
|
|
206
|
+
const existingEntity = await driver.fetchEntity({
|
|
207
|
+
path: collection.slug,
|
|
208
|
+
entityId: String(id),
|
|
209
|
+
collection: resolvedCollection
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (!existingEntity) {
|
|
213
|
+
throw ApiError.notFound("Entity not found");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await driver.deleteEntity({
|
|
217
|
+
entity: existingEntity,
|
|
218
|
+
collection: resolvedCollection
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return new Response(null, { status: 204 });
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Catch-all routes for subcollection paths.
|
|
227
|
+
*
|
|
228
|
+
* Matches URL patterns like:
|
|
229
|
+
* GET /authors/111094/posts → list child collection
|
|
230
|
+
* GET /authors/111094/posts/43 → get child entity
|
|
231
|
+
* POST /authors/111094/posts → create child entity
|
|
232
|
+
* PUT /authors/111094/posts/43 → update child entity
|
|
233
|
+
* DELETE /authors/111094/posts/43 → delete child entity
|
|
234
|
+
*
|
|
235
|
+
* The `:rest{.+}` regex param captures the full remainder of the URL
|
|
236
|
+
* path (Hono v4 `*` wildcard does not populate `c.req.param("*")`).
|
|
237
|
+
* We split it into segments and reconstruct the `collectionPath`
|
|
238
|
+
* (e.g. "authors/111094/posts") and optional `entityId` (e.g. "43").
|
|
239
|
+
*
|
|
240
|
+
* The DataDriver.saveEntity / fetchCollection / etc. already know how to
|
|
241
|
+
* resolve multi-segment relation paths, so we just forward to them.
|
|
242
|
+
*/
|
|
243
|
+
private createSubcollectionRoutes(): void {
|
|
244
|
+
// Reserved path segments that should NOT be treated as relation names.
|
|
245
|
+
// These are handled by dedicated route handlers (e.g., history routes)
|
|
246
|
+
// mounted on the same data router.
|
|
247
|
+
const RESERVED_SEGMENTS = new Set(["history"]);
|
|
248
|
+
|
|
249
|
+
// Helper: parse a path like "authors/111094/posts/43" into
|
|
250
|
+
// { collectionPath: "authors/111094/posts", entityId: "43" }
|
|
251
|
+
// or "authors/111094/posts" into
|
|
252
|
+
// { collectionPath: "authors/111094/posts", entityId: undefined }
|
|
253
|
+
const parseSubPath = (rawPath: string): { collectionPath: string; entityId?: string } | null => {
|
|
254
|
+
const segments = rawPath.split("/").filter(s => s && s !== "undefined");
|
|
255
|
+
// Need at least 3 segments for a subcollection path (parent/id/child)
|
|
256
|
+
if (segments.length < 3) return null;
|
|
257
|
+
|
|
258
|
+
// If any segment is a reserved path (e.g. "history"), this is not a
|
|
259
|
+
// subcollection route — let it fall through to other handlers.
|
|
260
|
+
if (segments.some(s => RESERVED_SEGMENTS.has(s))) return null;
|
|
261
|
+
|
|
262
|
+
// Odd segment count → collection path (parent/id/child or parent/id/child/id2/grandchild)
|
|
263
|
+
// Even segment count → entity path (parent/id/child/entityId)
|
|
264
|
+
if (segments.length % 2 === 1) {
|
|
265
|
+
return { collectionPath: segments.join("/") };
|
|
266
|
+
} else {
|
|
267
|
+
const entityId = segments.pop()!;
|
|
268
|
+
return { collectionPath: segments.join("/"), entityId };
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// GET /<subcollection-path> — list or get single entity
|
|
273
|
+
// Use :rest{.+} instead of * because Hono v4's wildcard doesn't
|
|
274
|
+
// capture into c.req.param("*") — it always returns undefined.
|
|
275
|
+
this.router.get("/:parent/:parentId/:rest{.+}", async (c, next) => {
|
|
276
|
+
const rest = c.req.param("rest");
|
|
277
|
+
if (!rest || rest === "undefined") return next();
|
|
278
|
+
const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
|
|
279
|
+
const parsed = parseSubPath(rawPath);
|
|
280
|
+
if (!parsed) return next();
|
|
281
|
+
|
|
282
|
+
const driver = c.get("driver") || this.driver;
|
|
283
|
+
|
|
284
|
+
if (parsed.entityId) {
|
|
285
|
+
// GET /parent/:parentId/child/:id — single entity
|
|
286
|
+
const entity = await driver.fetchEntity({
|
|
287
|
+
path: parsed.collectionPath,
|
|
288
|
+
entityId: parsed.entityId
|
|
289
|
+
});
|
|
290
|
+
if (!entity) throw ApiError.notFound("Entity not found");
|
|
291
|
+
return c.json(this.flattenEntity(entity));
|
|
292
|
+
} else {
|
|
293
|
+
// GET /parent/:parentId/child — list entities
|
|
294
|
+
const queryDict = c.req.query();
|
|
295
|
+
const queryOptions = parseQueryOptions(queryDict);
|
|
296
|
+
const entities = await driver.fetchCollection({
|
|
297
|
+
path: parsed.collectionPath,
|
|
298
|
+
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
299
|
+
limit: queryOptions.limit,
|
|
300
|
+
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
301
|
+
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
302
|
+
searchString: queryDict.searchString as string | undefined
|
|
303
|
+
});
|
|
304
|
+
return c.json({
|
|
305
|
+
data: entities.map(e => this.flattenEntity(e)),
|
|
306
|
+
meta: {
|
|
307
|
+
total: entities.length,
|
|
308
|
+
limit: queryOptions.limit,
|
|
309
|
+
offset: queryOptions.offset,
|
|
310
|
+
hasMore: false
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// POST /<subcollection-path> — create entity
|
|
317
|
+
this.router.post("/:parent/:parentId/:rest{.+}", async (c, next) => {
|
|
318
|
+
const rest = c.req.param("rest");
|
|
319
|
+
if (!rest || rest === "undefined") return next();
|
|
320
|
+
const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
|
|
321
|
+
const parsed = parseSubPath(rawPath);
|
|
322
|
+
if (!parsed || parsed.entityId) return next();
|
|
323
|
+
|
|
324
|
+
const driver = c.get("driver") || this.driver;
|
|
325
|
+
const body = await c.req.json().catch(() => ({}));
|
|
326
|
+
|
|
327
|
+
const entity = await driver.saveEntity({
|
|
328
|
+
path: parsed.collectionPath,
|
|
329
|
+
values: body,
|
|
330
|
+
status: "new"
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
return c.json(this.formatResponse(entity), 201);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// PUT /<subcollection-path>/:id — update entity
|
|
337
|
+
this.router.put("/:parent/:parentId/:rest{.+}", async (c, next) => {
|
|
338
|
+
const rest = c.req.param("rest");
|
|
339
|
+
if (!rest || rest === "undefined") return next();
|
|
340
|
+
const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
|
|
341
|
+
const parsed = parseSubPath(rawPath);
|
|
342
|
+
if (!parsed || !parsed.entityId) return next();
|
|
343
|
+
|
|
344
|
+
const driver = c.get("driver") || this.driver;
|
|
345
|
+
const body = await c.req.json().catch(() => ({}));
|
|
346
|
+
|
|
347
|
+
const entity = await driver.saveEntity({
|
|
348
|
+
path: parsed.collectionPath,
|
|
349
|
+
entityId: parsed.entityId,
|
|
350
|
+
values: body,
|
|
351
|
+
status: "existing"
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
return c.json(this.formatResponse(entity));
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// DELETE /<subcollection-path>/:id — delete entity
|
|
358
|
+
this.router.delete("/:parent/:parentId/:rest{.+}", async (c, next) => {
|
|
359
|
+
const rest = c.req.param("rest");
|
|
360
|
+
if (!rest || rest === "undefined") return next();
|
|
361
|
+
const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
|
|
362
|
+
const parsed = parseSubPath(rawPath);
|
|
363
|
+
if (!parsed || !parsed.entityId) return next();
|
|
364
|
+
|
|
365
|
+
const driver = c.get("driver") || this.driver;
|
|
366
|
+
|
|
367
|
+
const existingEntity = await driver.fetchEntity({
|
|
368
|
+
path: parsed.collectionPath,
|
|
369
|
+
entityId: parsed.entityId
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
if (!existingEntity) throw ApiError.notFound("Entity not found");
|
|
373
|
+
|
|
374
|
+
await driver.deleteEntity({ entity: existingEntity });
|
|
375
|
+
|
|
376
|
+
return new Response(null, { status: 204 });
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Format successful API response - flattened for traditional REST API
|
|
382
|
+
*/
|
|
383
|
+
private formatResponse<T>(data: T, meta?: Record<string, unknown>): unknown {
|
|
384
|
+
if (Array.isArray(data)) {
|
|
385
|
+
const flattenedData = data.map(entity => this.flattenEntity(entity));
|
|
386
|
+
if (meta) {
|
|
387
|
+
return {
|
|
388
|
+
data: flattenedData,
|
|
389
|
+
meta
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return flattenedData;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (data && typeof data === "object" && "values" in data) {
|
|
396
|
+
return this.flattenEntity(data as unknown as Entity<Record<string, unknown>>);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (meta) {
|
|
400
|
+
return {
|
|
401
|
+
data,
|
|
402
|
+
meta
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
return data;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Flatten Rebase entity structure to traditional REST format
|
|
410
|
+
*/
|
|
411
|
+
private flattenEntity(entity: Entity<Record<string, unknown>>): Record<string, unknown> {
|
|
412
|
+
if (!entity || typeof entity !== "object") {
|
|
413
|
+
return entity;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if ("values" in entity && typeof entity.values === "object") {
|
|
417
|
+
return {
|
|
418
|
+
id: entity.id,
|
|
419
|
+
...entity.values
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return entity as unknown as Record<string, unknown>;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Fetch raw collection data without Entity wrapper (fallback for non-Postgres)
|
|
428
|
+
*/
|
|
429
|
+
private async fetchRawCollection(driver: DataDriver, collection: EntityCollection, queryOptions: QueryOptions) {
|
|
430
|
+
const entities = await driver.fetchCollection({
|
|
431
|
+
path: collection.slug,
|
|
432
|
+
collection,
|
|
433
|
+
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
434
|
+
limit: queryOptions.limit,
|
|
435
|
+
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
436
|
+
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
437
|
+
startAfter: queryOptions.offset ? String(queryOptions.offset) : undefined
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
return entities.map(entity => this.flattenEntity(entity));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Count raw entities for a collection
|
|
445
|
+
*/
|
|
446
|
+
private async countRawEntities(driver: DataDriver, collection: EntityCollection, queryOptions: QueryOptions): Promise<number> {
|
|
447
|
+
return driver.countEntities ? await driver.countEntities({
|
|
448
|
+
path: collection.slug,
|
|
449
|
+
collection,
|
|
450
|
+
filter: queryOptions.where as FetchCollectionProps["filter"]
|
|
451
|
+
}) : 0;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Fetch single entity raw data without Entity wrapper (fallback)
|
|
456
|
+
*/
|
|
457
|
+
private async fetchRawEntity(driver: DataDriver, collection: EntityCollection, entityId: string) {
|
|
458
|
+
const entity = await driver.fetchEntity({
|
|
459
|
+
path: collection.slug,
|
|
460
|
+
entityId,
|
|
461
|
+
collection
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
return entity ? this.flattenEntity(entity) : null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { QueryOptions } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Map PostgREST-style operators to Rebase WhereFilterOp
|
|
5
|
+
*/
|
|
6
|
+
export function mapOperator(op: string): string | null {
|
|
7
|
+
switch (op) {
|
|
8
|
+
case "eq": return "==";
|
|
9
|
+
case "neq": return "!=";
|
|
10
|
+
case "gt": return ">";
|
|
11
|
+
case "gte": return ">=";
|
|
12
|
+
case "lt": return "<";
|
|
13
|
+
case "lte": return "<=";
|
|
14
|
+
case "in": return "in";
|
|
15
|
+
case "nin": return "not-in";
|
|
16
|
+
case "cs": return "array-contains";
|
|
17
|
+
case "csa": return "array-contains-any";
|
|
18
|
+
default: return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse query parameters into QueryOptions
|
|
24
|
+
*/
|
|
25
|
+
export function parseQueryOptions(query: Record<string, unknown>): QueryOptions {
|
|
26
|
+
const options: QueryOptions = {};
|
|
27
|
+
|
|
28
|
+
// Pagination
|
|
29
|
+
if (query.limit) options.limit = parseInt(String(query.limit));
|
|
30
|
+
if (query.offset) options.offset = parseInt(String(query.offset));
|
|
31
|
+
if (query.page) {
|
|
32
|
+
const page = parseInt(String(query.page));
|
|
33
|
+
const limit = options.limit || 20;
|
|
34
|
+
options.offset = (page - 1) * limit;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Filtering
|
|
38
|
+
options.where = {};
|
|
39
|
+
|
|
40
|
+
// Legacy JSON where clause
|
|
41
|
+
if (query.where) {
|
|
42
|
+
try {
|
|
43
|
+
const parsedWhere = typeof query.where === "string"
|
|
44
|
+
? JSON.parse(query.where)
|
|
45
|
+
: query.where;
|
|
46
|
+
if (typeof parsedWhere !== "object" || parsedWhere === null || Array.isArray(parsedWhere)) {
|
|
47
|
+
throw new Error("Filter must be a JSON object");
|
|
48
|
+
}
|
|
49
|
+
Object.assign(options.where, parsedWhere);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
const message = e instanceof Error ? e.message : "malformed JSON";
|
|
52
|
+
const err = new Error(`Invalid 'where' filter: ${message}`) as Error & { code?: string; statusCode?: number };
|
|
53
|
+
err.code = "BAD_REQUEST";
|
|
54
|
+
err.statusCode = 400;
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// PostgREST style filtering
|
|
60
|
+
const reservedQueryKeys = ["limit", "offset", "page", "orderBy", "where", "include", "fields"];
|
|
61
|
+
for (const [key, rawValue] of Object.entries(query)) {
|
|
62
|
+
if (reservedQueryKeys.includes(key)) continue;
|
|
63
|
+
|
|
64
|
+
const value = Array.isArray(rawValue) ? rawValue[rawValue.length - 1] : rawValue;
|
|
65
|
+
|
|
66
|
+
if (typeof value === "string") {
|
|
67
|
+
const parts = value.split(".");
|
|
68
|
+
if (parts.length >= 2) {
|
|
69
|
+
const op = parts[0];
|
|
70
|
+
const val = parts.slice(1).join(".");
|
|
71
|
+
const rebaseOp = mapOperator(op);
|
|
72
|
+
|
|
73
|
+
if (rebaseOp) {
|
|
74
|
+
let parsedVal: string | number | boolean | null | (string | number | boolean | null)[] = val;
|
|
75
|
+
// Attempt to parse primitive types or arrays
|
|
76
|
+
if (val === "true") parsedVal = true;
|
|
77
|
+
else if (val === "false") parsedVal = false;
|
|
78
|
+
else if (val === "null") parsedVal = null;
|
|
79
|
+
else if (!isNaN(Number(val)) && val.trim() !== "") parsedVal = Number(val);
|
|
80
|
+
else if (val.startsWith("(")) {
|
|
81
|
+
// Array for 'in' or 'not-in' ops (e.g. (1,2,3) or (a,b,c))
|
|
82
|
+
const arrayContent = val.endsWith(")") ? val.slice(1, -1) : val.slice(1);
|
|
83
|
+
parsedVal = arrayContent.split(",").map(v => {
|
|
84
|
+
const trimmed = v.trim();
|
|
85
|
+
if (!isNaN(Number(trimmed)) && trimmed !== "") return Number(trimmed);
|
|
86
|
+
if (trimmed === "true") return true;
|
|
87
|
+
if (trimmed === "false") return false;
|
|
88
|
+
if (trimmed === "null") return null;
|
|
89
|
+
return trimmed;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
options.where[key] = [rebaseOp, parsedVal];
|
|
94
|
+
} else {
|
|
95
|
+
// Fallback: assume implicit eq if the dot wasn't an operator (e.g. email or float)
|
|
96
|
+
let parsedVal: string | number | boolean | null = value;
|
|
97
|
+
if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
|
|
98
|
+
options.where[key] = ["==", parsedVal];
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// Implicit eq
|
|
102
|
+
let parsedVal: string | number | boolean | null = value;
|
|
103
|
+
if (value === "true") parsedVal = true;
|
|
104
|
+
else if (value === "false") parsedVal = false;
|
|
105
|
+
else if (value === "null") parsedVal = null;
|
|
106
|
+
else if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
|
|
107
|
+
|
|
108
|
+
options.where[key] = ["==", parsedVal];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (Object.keys(options.where).length === 0) {
|
|
114
|
+
delete options.where;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Sorting
|
|
118
|
+
if (query.orderBy) {
|
|
119
|
+
try {
|
|
120
|
+
options.orderBy = typeof query.orderBy === "string"
|
|
121
|
+
? JSON.parse(query.orderBy)
|
|
122
|
+
: query.orderBy;
|
|
123
|
+
} catch {
|
|
124
|
+
// Try simple format: "field:direction"
|
|
125
|
+
if (typeof query.orderBy === "string") {
|
|
126
|
+
const [field, direction] = query.orderBy.split(":");
|
|
127
|
+
const dir = (direction === "desc" ? "desc" : "asc") as "asc" | "desc";
|
|
128
|
+
options.orderBy = [
|
|
129
|
+
{
|
|
130
|
+
field,
|
|
131
|
+
direction: dir
|
|
132
|
+
}
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Relation includes
|
|
139
|
+
if (query.include) {
|
|
140
|
+
const includeStr = String(query.include).trim();
|
|
141
|
+
if (includeStr === "*") {
|
|
142
|
+
options.include = ["*"];
|
|
143
|
+
} else {
|
|
144
|
+
options.include = includeStr.split(",").map(s => s.trim()).filter(Boolean);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Field selection
|
|
149
|
+
if (query.fields) {
|
|
150
|
+
const fieldsStr = String(query.fields).trim();
|
|
151
|
+
options.fields = fieldsStr.split(",").map(s => s.trim()).filter(Boolean);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return options;
|
|
155
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { AstSchemaEditor } from "./ast-schema-editor";
|
|
3
|
+
import { HonoEnv } from "./types";
|
|
4
|
+
|
|
5
|
+
export function createSchemaEditorRoutes(collectionsDir: string): Hono<HonoEnv> {
|
|
6
|
+
const router = new Hono<HonoEnv>();
|
|
7
|
+
const editor = new AstSchemaEditor(collectionsDir);
|
|
8
|
+
|
|
9
|
+
router.post("/property/save", async (c) => {
|
|
10
|
+
const body = await c.req.json();
|
|
11
|
+
const { collectionId, propertyKey, propertyConfig } = body;
|
|
12
|
+
await editor.saveProperty(collectionId, propertyKey, propertyConfig);
|
|
13
|
+
return c.json({ success: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
router.post("/property/delete", async (c) => {
|
|
17
|
+
const body = await c.req.json();
|
|
18
|
+
const { collectionId, propertyKey } = body;
|
|
19
|
+
await editor.deleteProperty(collectionId, propertyKey);
|
|
20
|
+
return c.json({ success: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
router.post("/collection/save", async (c) => {
|
|
24
|
+
const body = await c.req.json();
|
|
25
|
+
const { collectionId, collectionData } = body;
|
|
26
|
+
await editor.saveCollection(collectionId, collectionData);
|
|
27
|
+
return c.json({ success: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
router.post("/collection/delete", async (c) => {
|
|
31
|
+
const body = await c.req.json();
|
|
32
|
+
const { collectionId } = body;
|
|
33
|
+
await editor.deleteCollection(collectionId);
|
|
34
|
+
return c.json({ success: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return router;
|
|
38
|
+
}
|
|
39
|
+
|