@rebasepro/server-core 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -25
- package/dist/common/src/collections/default-collections.d.ts +5 -8
- package/dist/common/src/data/query_builder.d.ts +6 -2
- package/dist/common/src/util/permissions.d.ts +14 -6
- package/dist/index.es.js +393 -315
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +393 -315
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/errors.d.ts +15 -0
- package/dist/server-core/src/api/rest/query-parser.d.ts +2 -0
- package/dist/server-core/src/api/types.d.ts +2 -1
- package/dist/server-core/src/auth/jwt.d.ts +10 -0
- package/dist/server-core/src/email/types.d.ts +1 -0
- package/dist/server-core/src/init.d.ts +31 -1
- package/dist/types/src/controllers/auth.d.ts +2 -2
- package/dist/types/src/controllers/client.d.ts +25 -40
- package/dist/types/src/controllers/data.d.ts +21 -3
- package/dist/types/src/controllers/data_driver.d.ts +5 -0
- package/dist/types/src/controllers/email.d.ts +2 -0
- package/dist/types/src/types/auth_adapter.d.ts +3 -56
- package/dist/types/src/types/backend.d.ts +38 -3
- package/dist/types/src/types/backend_hooks.d.ts +2 -17
- package/dist/types/src/types/collections.d.ts +30 -6
- package/dist/types/src/types/entity_views.d.ts +19 -28
- package/dist/types/src/types/properties.d.ts +9 -15
- package/dist/types/src/types/user_management_delegate.d.ts +16 -53
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -1
- package/package.json +5 -5
- package/src/api/errors.ts +20 -1
- package/src/api/openapi-generator.ts +1 -1
- package/src/api/rest/api-generator.ts +82 -24
- package/src/api/rest/query-parser.ts +184 -63
- package/src/api/server.ts +1 -1
- package/src/api/types.ts +2 -1
- package/src/auth/admin-routes.ts +2 -91
- package/src/auth/builtin-auth-adapter.ts +9 -70
- package/src/auth/custom-auth-adapter.ts +1 -1
- package/src/auth/jwt.ts +10 -0
- package/src/auth/routes.ts +5 -9
- package/src/email/smtp-email-service.ts +31 -0
- package/src/email/types.ts +1 -0
- package/src/init.ts +135 -31
- package/src/storage/image-transform.ts +2 -1
- package/test/admin-routes.test.ts +0 -169
- package/test/backend-hooks-admin.test.ts +0 -25
- package/test/custom-auth-adapter.test.ts +2 -10
- package/test/smtp-email-service.test.ts +169 -0
- package/dist/types/src/users/roles.d.ts +0 -14
- package/test.ts +0 -6
|
@@ -56,41 +56,32 @@ export type EntityCustomView<M extends Record<string, unknown> = Record<string,
|
|
|
56
56
|
Builder?: ComponentRef<EntityCustomViewParams<M>>;
|
|
57
57
|
position?: "start" | "end";
|
|
58
58
|
};
|
|
59
|
-
export interface EntityCustomViewParams<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
60
|
-
collection: EntityCollection<M>;
|
|
61
|
-
entity?: Entity<M>;
|
|
62
|
-
modifiedValues?: EntityValues<M>;
|
|
63
|
-
formContext: FormContext<M>;
|
|
64
|
-
parentCollectionSlugs?: string[];
|
|
65
|
-
parentEntityIds?: string[];
|
|
66
|
-
}
|
|
67
59
|
/**
|
|
68
|
-
* Configuration
|
|
69
|
-
*
|
|
60
|
+
* Configuration to replace the default entity form with a custom component.
|
|
61
|
+
* The Builder receives the same props as entity view tabs (entity, formContext, etc.)
|
|
62
|
+
* and has full control over the UI.
|
|
63
|
+
*
|
|
64
|
+
* The form tab still appears in the tab bar but renders your Builder
|
|
65
|
+
* instead of the auto-generated field form.
|
|
66
|
+
*
|
|
70
67
|
* @group Models
|
|
71
68
|
*/
|
|
72
|
-
export type
|
|
73
|
-
/**
|
|
74
|
-
* Custom component rendered above the property display in the detail view.
|
|
75
|
-
*/
|
|
76
|
-
Header?: ComponentRef<EntityDetailViewParams<M>>;
|
|
69
|
+
export type FormViewConfig<M extends Record<string, unknown> = Record<string, unknown>> = {
|
|
77
70
|
/**
|
|
78
|
-
* Custom component
|
|
71
|
+
* Custom component that replaces the default form.
|
|
79
72
|
*/
|
|
80
|
-
|
|
73
|
+
Builder: ComponentRef<EntityCustomViewParams<M>>;
|
|
81
74
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
75
|
+
* If true, the save/delete action bar is rendered alongside the custom view.
|
|
76
|
+
* Defaults to true.
|
|
84
77
|
*/
|
|
85
|
-
|
|
78
|
+
includeActions?: boolean;
|
|
86
79
|
};
|
|
87
|
-
|
|
88
|
-
* Props passed to detail view customization components (Header, Footer, Builder).
|
|
89
|
-
* @group Models
|
|
90
|
-
*/
|
|
91
|
-
export interface EntityDetailViewParams<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
80
|
+
export interface EntityCustomViewParams<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
92
81
|
collection: EntityCollection<M>;
|
|
93
|
-
entity
|
|
94
|
-
|
|
95
|
-
|
|
82
|
+
entity?: Entity<M>;
|
|
83
|
+
modifiedValues?: EntityValues<M>;
|
|
84
|
+
formContext: FormContext<M>;
|
|
85
|
+
parentCollectionSlugs?: string[];
|
|
86
|
+
parentEntityIds?: string[];
|
|
96
87
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentRef } from "./component_ref";
|
|
2
|
-
import type { EntityReference, EntityRelation, EntityValues, GeoPoint,
|
|
3
|
-
import type {
|
|
2
|
+
import type { Entity, EntityReference, EntityRelation, EntityValues, GeoPoint, Vector } from "./entities";
|
|
3
|
+
import type { JoinStep, OnAction, Relation } from "./relations";
|
|
4
4
|
import type { EntityCollection, FilterValues } from "./collections";
|
|
5
5
|
import type { ColorKey, ColorScheme } from "./chips";
|
|
6
6
|
import type { AuthController } from "../controllers/auth";
|
|
@@ -104,8 +104,8 @@ export interface BaseUIConfig<CustomProps = unknown> {
|
|
|
104
104
|
disabled?: boolean | PropertyDisabledConfig;
|
|
105
105
|
widthPercentage?: number;
|
|
106
106
|
customProps?: CustomProps;
|
|
107
|
-
Field?: ComponentRef
|
|
108
|
-
Preview?: ComponentRef
|
|
107
|
+
Field?: ComponentRef<any>;
|
|
108
|
+
Preview?: ComponentRef<any>;
|
|
109
109
|
}
|
|
110
110
|
export interface BaseProperty<CustomProps = unknown> {
|
|
111
111
|
ui?: BaseUIConfig<CustomProps>;
|
|
@@ -185,7 +185,7 @@ export interface StringProperty extends BaseProperty {
|
|
|
185
185
|
* Optional database column type. If not set, it defaults to `varchar` or `uuid` depending on `isId` configuration.
|
|
186
186
|
* Use `text` for strings with unbound length, `char` for fixed-length strings, or `varchar` for variable-length strings with a limit.
|
|
187
187
|
*/
|
|
188
|
-
columnType?: "varchar" | "text" | "char";
|
|
188
|
+
columnType?: "varchar" | "text" | "char" | "uuid";
|
|
189
189
|
/**
|
|
190
190
|
* Rules for validating this property
|
|
191
191
|
*/
|
|
@@ -541,9 +541,11 @@ export interface ArrayProperty extends BaseProperty {
|
|
|
541
541
|
ui?: ArrayUIConfig;
|
|
542
542
|
type: "array";
|
|
543
543
|
/**
|
|
544
|
-
* Optional database column type.
|
|
544
|
+
* Optional database column type. By default, maps to a native Postgres array
|
|
545
|
+
* (e.g. `text[]`, `integer[]`/`numeric[]`, `boolean[]`) if the element type
|
|
546
|
+
* is a primitive, otherwise defaults to `jsonb`.
|
|
545
547
|
*/
|
|
546
|
-
columnType?: "json" | "jsonb";
|
|
548
|
+
columnType?: "json" | "jsonb" | "text[]" | "integer[]" | "boolean[]" | "numeric[]";
|
|
547
549
|
/**
|
|
548
550
|
* The property of this array.
|
|
549
551
|
* You can specify any property (except another Array property)
|
|
@@ -639,14 +641,6 @@ export interface MapProperty extends BaseProperty {
|
|
|
639
641
|
* Properties that are displayed when rendered as a preview
|
|
640
642
|
*/
|
|
641
643
|
previewProperties?: string[];
|
|
642
|
-
/**
|
|
643
|
-
* Allow the user to add only some keys in this map.
|
|
644
|
-
* By default, all properties of the map have the corresponding field in
|
|
645
|
-
* the form view. Setting this flag to true allows to pick only some.
|
|
646
|
-
* Useful for map that can have a lot of sub-properties that may not be
|
|
647
|
-
* needed
|
|
648
|
-
*/
|
|
649
|
-
pickOnlySomeKeys?: boolean;
|
|
650
644
|
/**
|
|
651
645
|
* Render this map as a key-value table that allows to use
|
|
652
646
|
* arbitrary keys. You don't need to define the properties in this case.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { User } from "../users";
|
|
2
2
|
/**
|
|
3
3
|
* Result of creating a new user via admin flow.
|
|
4
4
|
* Contains the created user plus information about how credentials were delivered.
|
|
@@ -15,56 +15,46 @@ export interface UserCreationResult<USER extends User = User> {
|
|
|
15
15
|
temporaryPassword?: string;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* Delegate to manage
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Delegate to manage auth-specific user operations.
|
|
19
|
+
*
|
|
20
|
+
* This interface allows the CMS to be agnostic of the underlying
|
|
21
|
+
* authentication provider or backend. User/role CRUD is now handled
|
|
22
|
+
* by the collection system; this delegate only exposes auth-specific
|
|
23
|
+
* operations (password hashing, invitations, bootstrap).
|
|
21
24
|
*
|
|
22
25
|
* @group Models
|
|
23
26
|
*/
|
|
24
27
|
export interface UserManagementDelegate<USER extends User = User> {
|
|
25
28
|
/**
|
|
26
|
-
* Are
|
|
29
|
+
* Are auth-related operations currently loading?
|
|
27
30
|
*/
|
|
28
31
|
loading: boolean;
|
|
29
32
|
/**
|
|
30
|
-
*
|
|
33
|
+
* In-memory list of users (used for client-side filtering fallback).
|
|
31
34
|
*/
|
|
32
|
-
users
|
|
35
|
+
users?: USER[];
|
|
33
36
|
/**
|
|
34
|
-
*
|
|
37
|
+
* Error from fetching the users list, if any.
|
|
35
38
|
*/
|
|
36
39
|
usersError?: Error;
|
|
37
40
|
/**
|
|
38
|
-
*
|
|
39
|
-
* user information when assigning ownership of an entity.
|
|
40
|
-
* @param uid
|
|
41
|
+
* Look up a single user by UID from the in-memory cache.
|
|
41
42
|
*/
|
|
42
|
-
getUser
|
|
43
|
+
getUser?: (uid: string) => USER | null;
|
|
43
44
|
/**
|
|
44
|
-
*
|
|
45
|
-
* When provided, the CMS will use this for the users table
|
|
46
|
-
* instead of loading all users into memory.
|
|
45
|
+
* Server-side user search with pagination.
|
|
47
46
|
*/
|
|
48
|
-
searchUsers?: (
|
|
47
|
+
searchUsers?: (params: {
|
|
49
48
|
search?: string;
|
|
50
49
|
limit?: number;
|
|
51
50
|
offset?: number;
|
|
52
|
-
orderBy?: string;
|
|
53
|
-
orderDir?: "asc" | "desc";
|
|
54
|
-
roleId?: string;
|
|
55
51
|
}) => Promise<{
|
|
56
52
|
users: USER[];
|
|
57
53
|
total: number;
|
|
58
54
|
}>;
|
|
59
|
-
/**
|
|
60
|
-
* Save a user (create or update)
|
|
61
|
-
* @param user
|
|
62
|
-
*/
|
|
63
|
-
saveUser?: (user: USER) => Promise<USER>;
|
|
64
55
|
/**
|
|
65
56
|
* Create a new user with invitation/password generation support.
|
|
66
57
|
* Returns additional info about how the credentials were delivered.
|
|
67
|
-
* Falls back to saveUser if not provided.
|
|
68
58
|
*/
|
|
69
59
|
createUser?: (user: USER) => Promise<UserCreationResult<USER>>;
|
|
70
60
|
/**
|
|
@@ -73,42 +63,15 @@ export interface UserManagementDelegate<USER extends User = User> {
|
|
|
73
63
|
* or a flag indicating an email invitation was sent.
|
|
74
64
|
*/
|
|
75
65
|
resetPassword?: (user: USER) => Promise<UserCreationResult<USER>>;
|
|
76
|
-
/**
|
|
77
|
-
* Delete a user
|
|
78
|
-
* @param user
|
|
79
|
-
*/
|
|
80
|
-
deleteUser?: (user: USER) => Promise<void>;
|
|
81
|
-
/**
|
|
82
|
-
* List of roles defined in the CMS.
|
|
83
|
-
*/
|
|
84
|
-
roles?: Role[];
|
|
85
|
-
/**
|
|
86
|
-
* Optional error if roles failed to load.
|
|
87
|
-
*/
|
|
88
|
-
rolesError?: Error;
|
|
89
|
-
/**
|
|
90
|
-
* Save a role (create or update)
|
|
91
|
-
* @param role
|
|
92
|
-
*/
|
|
93
|
-
saveRole?: (role: Role) => Promise<void>;
|
|
94
|
-
/**
|
|
95
|
-
* Delete a role
|
|
96
|
-
* @param role
|
|
97
|
-
*/
|
|
98
|
-
deleteRole?: (role: Role) => Promise<void>;
|
|
99
66
|
/**
|
|
100
67
|
* Is the currently logged in user an admin?
|
|
101
68
|
*/
|
|
102
69
|
isAdmin?: boolean;
|
|
103
|
-
/**
|
|
104
|
-
* If true, the UI will allow the user to create the default roles (admin, editor, viewer).
|
|
105
|
-
*/
|
|
106
|
-
allowDefaultRolesCreation?: boolean;
|
|
107
70
|
/**
|
|
108
71
|
* Optionally define roles for a given user. This is useful when the roles
|
|
109
72
|
* are coming from a separate provider than the one issuing the tokens.
|
|
110
73
|
*/
|
|
111
|
-
defineRolesFor?: (user: USER) => Promise<
|
|
74
|
+
defineRolesFor?: (user: USER) => Promise<string[] | undefined> | string[] | undefined;
|
|
112
75
|
/**
|
|
113
76
|
* Whether any admin users exist. Used by the bootstrap banner to decide
|
|
114
77
|
* whether to prompt. Populated via a lightweight check (e.g. `limit=1`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "Database-Agnostic Backend Core for Rebase",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"ts-morph": "27.0.2",
|
|
55
55
|
"ws": "^8.20.1",
|
|
56
56
|
"zod": "^3.25.76",
|
|
57
|
-
"@rebasepro/common": "0.
|
|
58
|
-
"@rebasepro/
|
|
59
|
-
"@rebasepro/
|
|
60
|
-
"@rebasepro/utils": "0.
|
|
57
|
+
"@rebasepro/common": "0.5.0",
|
|
58
|
+
"@rebasepro/client": "0.5.0",
|
|
59
|
+
"@rebasepro/types": "0.5.0",
|
|
60
|
+
"@rebasepro/utils": "0.5.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/jest": "^29.5.14",
|
package/src/api/errors.ts
CHANGED
|
@@ -70,13 +70,32 @@ export interface ErrorResponse {
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* General shape of errors that flow through the API error handler.
|
|
75
|
+
* Extends Error with optional HTTP status, error code, and details.
|
|
76
|
+
*/
|
|
77
|
+
export interface RebaseApiError extends Error {
|
|
78
|
+
statusCode?: number;
|
|
79
|
+
code?: string;
|
|
80
|
+
details?: unknown;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Type guard for errors that carry optional API metadata (statusCode, code, details).
|
|
85
|
+
* Returns true for any Error instance — the optional properties are then
|
|
86
|
+
* checked via normal property access.
|
|
87
|
+
*/
|
|
88
|
+
export function isRebaseApiError(error: unknown): error is RebaseApiError {
|
|
89
|
+
return error instanceof Error;
|
|
90
|
+
}
|
|
91
|
+
|
|
73
92
|
/**
|
|
74
93
|
* Hono error-handling middleware (`app.onError`).
|
|
75
94
|
* Converts any error into the canonical `{ error: { message, code } }` shape.
|
|
76
95
|
*/
|
|
77
96
|
export const errorHandler: ErrorHandler = (err, c) => {
|
|
78
97
|
// Typecast custom error properties
|
|
79
|
-
const error = err
|
|
98
|
+
const error: RebaseApiError = err;
|
|
80
99
|
|
|
81
100
|
if (error instanceof ApiError || error.name === "ApiError") {
|
|
82
101
|
// Operational errors — log at warn level
|
|
@@ -314,7 +314,7 @@ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResp
|
|
|
314
314
|
};
|
|
315
315
|
|
|
316
316
|
// ── Subcollection routes ──────────────────────────────────────
|
|
317
|
-
const relations =
|
|
317
|
+
const relations = collection.relations;
|
|
318
318
|
if (relations && relations.length > 0) {
|
|
319
319
|
for (const relation of relations) {
|
|
320
320
|
const relationName = relation.relationName;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { DataDriver, Entity, EntityCollection, FetchCollectionProps, DataHooks, BackendHookContext, RestFetchService } from "@rebasepro/types";
|
|
3
3
|
import { QueryOptions, HonoEnv } from "../types";
|
|
4
|
-
import { ApiError } from "../errors";
|
|
4
|
+
import { ApiError, isRebaseApiError } from "../errors";
|
|
5
5
|
import { parseQueryOptions } from "./query-parser";
|
|
6
6
|
import { httpMethodToOperation, isOperationAllowed } from "../../auth/api-keys/api-key-permission-guard";
|
|
7
7
|
import type { ApiKeyMasked } from "../../auth/api-keys/api-key-types";
|
|
@@ -87,9 +87,9 @@ export class RestApiGenerator {
|
|
|
87
87
|
// GET /collection/count - Count entities (with optional filters)
|
|
88
88
|
this.router.get(`${basePath}/count`, async (c) => {
|
|
89
89
|
this.enforceApiKeyPermission(c, collection.slug);
|
|
90
|
-
const queryDict = c.req.
|
|
90
|
+
const queryDict = c.req.queries();
|
|
91
91
|
const queryOptions = parseQueryOptions(queryDict);
|
|
92
|
-
const searchString = queryDict.searchString
|
|
92
|
+
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : undefined;
|
|
93
93
|
const driver = c.get("driver") || this.driver;
|
|
94
94
|
|
|
95
95
|
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
|
|
@@ -99,9 +99,9 @@ export class RestApiGenerator {
|
|
|
99
99
|
// GET /collection - List entities
|
|
100
100
|
this.router.get(basePath, async (c) => {
|
|
101
101
|
this.enforceApiKeyPermission(c, collection.slug);
|
|
102
|
-
const queryDict = c.req.
|
|
102
|
+
const queryDict = c.req.queries();
|
|
103
103
|
const queryOptions = parseQueryOptions(queryDict);
|
|
104
|
-
const searchString = queryDict.searchString
|
|
104
|
+
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : undefined;
|
|
105
105
|
|
|
106
106
|
const driver = c.get("driver") || this.driver;
|
|
107
107
|
const fetchService = this.getFetchService(driver);
|
|
@@ -161,7 +161,7 @@ export class RestApiGenerator {
|
|
|
161
161
|
this.router.get(`${basePath}/:id`, async (c) => {
|
|
162
162
|
this.enforceApiKeyPermission(c, collection.slug);
|
|
163
163
|
const id = c.req.param("id");
|
|
164
|
-
const queryDict = c.req.
|
|
164
|
+
const queryDict = c.req.queries();
|
|
165
165
|
const queryOptions = parseQueryOptions(queryDict);
|
|
166
166
|
const driver = c.get("driver") || this.driver;
|
|
167
167
|
const fetchService = this.getFetchService(driver);
|
|
@@ -234,9 +234,10 @@ export class RestApiGenerator {
|
|
|
234
234
|
|
|
235
235
|
return c.json(response, 201);
|
|
236
236
|
} catch (error) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
if (isRebaseApiError(error) && !error.code) {
|
|
238
|
+
error.code = "BAD_REQUEST";
|
|
239
|
+
}
|
|
240
|
+
throw error;
|
|
240
241
|
}
|
|
241
242
|
});
|
|
242
243
|
|
|
@@ -282,9 +283,10 @@ export class RestApiGenerator {
|
|
|
282
283
|
|
|
283
284
|
return c.json(response);
|
|
284
285
|
} catch (error) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
if (isRebaseApiError(error) && !error.code) {
|
|
287
|
+
error.code = "BAD_REQUEST";
|
|
288
|
+
}
|
|
289
|
+
throw error;
|
|
288
290
|
}
|
|
289
291
|
});
|
|
290
292
|
|
|
@@ -386,15 +388,18 @@ entityId };
|
|
|
386
388
|
|
|
387
389
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
388
390
|
|
|
391
|
+
const hookCtx = this.buildHookContext(c, "GET");
|
|
392
|
+
|
|
389
393
|
if (parsed.entityId === "count") {
|
|
390
394
|
// GET /parent/:parentId/child/count — count child entities
|
|
391
|
-
const queryDict = c.req.
|
|
395
|
+
const queryDict = c.req.queries();
|
|
392
396
|
const queryOptions = parseQueryOptions(queryDict);
|
|
397
|
+
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : undefined;
|
|
393
398
|
|
|
394
399
|
const total = driver.countEntities ? await driver.countEntities({
|
|
395
400
|
path: parsed.collectionPath,
|
|
396
401
|
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
397
|
-
searchString
|
|
402
|
+
searchString
|
|
398
403
|
}) : 0;
|
|
399
404
|
|
|
400
405
|
return c.json({ count: total });
|
|
@@ -405,26 +410,42 @@ entityId };
|
|
|
405
410
|
entityId: parsed.entityId
|
|
406
411
|
});
|
|
407
412
|
if (!entity) throw ApiError.notFound("Entity not found");
|
|
408
|
-
|
|
413
|
+
|
|
414
|
+
const flatResult = this.flattenEntity(entity);
|
|
415
|
+
const hookResult = await this.applyAfterRead(c.req.param("parent"), flatResult, hookCtx);
|
|
416
|
+
if (!hookResult) throw ApiError.notFound("Entity not found");
|
|
417
|
+
|
|
418
|
+
return c.json(hookResult);
|
|
409
419
|
} else {
|
|
410
420
|
// GET /parent/:parentId/child — list entities
|
|
411
|
-
const queryDict = c.req.
|
|
421
|
+
const queryDict = c.req.queries();
|
|
412
422
|
const queryOptions = parseQueryOptions(queryDict);
|
|
423
|
+
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : undefined;
|
|
413
424
|
const entities = await driver.fetchCollection({
|
|
414
425
|
path: parsed.collectionPath,
|
|
415
426
|
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
416
427
|
limit: queryOptions.limit,
|
|
417
428
|
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
418
429
|
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
419
|
-
searchString
|
|
430
|
+
searchString
|
|
420
431
|
});
|
|
432
|
+
|
|
433
|
+
let flatEntities = entities.map(e => this.flattenEntity(e));
|
|
434
|
+
flatEntities = await this.applyAfterReadBatch(c.req.param("parent"), flatEntities, hookCtx);
|
|
435
|
+
|
|
436
|
+
const total = driver.countEntities ? await driver.countEntities({
|
|
437
|
+
path: parsed.collectionPath,
|
|
438
|
+
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
439
|
+
searchString
|
|
440
|
+
}) : flatEntities.length;
|
|
441
|
+
|
|
421
442
|
return c.json({
|
|
422
|
-
data:
|
|
443
|
+
data: flatEntities,
|
|
423
444
|
meta: {
|
|
424
|
-
total
|
|
445
|
+
total,
|
|
425
446
|
limit: queryOptions.limit,
|
|
426
447
|
offset: queryOptions.offset,
|
|
427
|
-
hasMore:
|
|
448
|
+
hasMore: (queryOptions.offset || 0) + flatEntities.length < total
|
|
428
449
|
}
|
|
429
450
|
});
|
|
430
451
|
}
|
|
@@ -439,9 +460,14 @@ entityId };
|
|
|
439
460
|
if (!parsed || parsed.entityId) return next();
|
|
440
461
|
|
|
441
462
|
const driver = c.get("driver") || this.driver;
|
|
463
|
+
const hookCtx = this.buildHookContext(c, "POST");
|
|
442
464
|
|
|
443
465
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
444
|
-
|
|
466
|
+
let body = await c.req.json().catch(() => ({}));
|
|
467
|
+
|
|
468
|
+
if (this.dataHooks?.beforeSave) {
|
|
469
|
+
body = await this.dataHooks.beforeSave(parsed.collectionPath, body, undefined, hookCtx);
|
|
470
|
+
}
|
|
445
471
|
|
|
446
472
|
const entity = await driver.saveEntity({
|
|
447
473
|
path: parsed.collectionPath,
|
|
@@ -449,7 +475,15 @@ entityId };
|
|
|
449
475
|
status: "new"
|
|
450
476
|
});
|
|
451
477
|
|
|
452
|
-
|
|
478
|
+
const response = this.formatResponse(entity);
|
|
479
|
+
|
|
480
|
+
if (this.dataHooks?.afterSave) {
|
|
481
|
+
Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
482
|
+
console.error("[BackendHooks] data.afterSave error:", err instanceof Error ? err.message : err);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return c.json(response, 201);
|
|
453
487
|
});
|
|
454
488
|
|
|
455
489
|
// PUT /<subcollection-path>/:id — update entity
|
|
@@ -461,10 +495,15 @@ entityId };
|
|
|
461
495
|
if (!parsed || !parsed.entityId) return next();
|
|
462
496
|
|
|
463
497
|
const driver = c.get("driver") || this.driver;
|
|
498
|
+
const hookCtx = this.buildHookContext(c, "PUT");
|
|
464
499
|
|
|
465
500
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
466
501
|
|
|
467
|
-
|
|
502
|
+
let body = await c.req.json().catch(() => ({}));
|
|
503
|
+
|
|
504
|
+
if (this.dataHooks?.beforeSave) {
|
|
505
|
+
body = await this.dataHooks.beforeSave(parsed.collectionPath, body, parsed.entityId, hookCtx);
|
|
506
|
+
}
|
|
468
507
|
|
|
469
508
|
const entity = await driver.saveEntity({
|
|
470
509
|
path: parsed.collectionPath,
|
|
@@ -473,7 +512,15 @@ entityId };
|
|
|
473
512
|
status: "existing"
|
|
474
513
|
});
|
|
475
514
|
|
|
476
|
-
|
|
515
|
+
const response = this.formatResponse(entity);
|
|
516
|
+
|
|
517
|
+
if (this.dataHooks?.afterSave) {
|
|
518
|
+
Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
519
|
+
console.error("[BackendHooks] data.afterSave error:", err instanceof Error ? err.message : err);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return c.json(response);
|
|
477
524
|
});
|
|
478
525
|
|
|
479
526
|
// DELETE /<subcollection-path>/:id — delete entity
|
|
@@ -485,6 +532,7 @@ entityId };
|
|
|
485
532
|
if (!parsed || !parsed.entityId) return next();
|
|
486
533
|
|
|
487
534
|
const driver = c.get("driver") || this.driver;
|
|
535
|
+
const hookCtx = this.buildHookContext(c, "DELETE");
|
|
488
536
|
|
|
489
537
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
490
538
|
|
|
@@ -495,8 +543,18 @@ entityId };
|
|
|
495
543
|
|
|
496
544
|
if (!existingEntity) throw ApiError.notFound("Entity not found");
|
|
497
545
|
|
|
546
|
+
if (this.dataHooks?.beforeDelete) {
|
|
547
|
+
await this.dataHooks.beforeDelete(parsed.collectionPath, parsed.entityId, hookCtx);
|
|
548
|
+
}
|
|
549
|
+
|
|
498
550
|
await driver.deleteEntity({ entity: existingEntity });
|
|
499
551
|
|
|
552
|
+
if (this.dataHooks?.afterDelete) {
|
|
553
|
+
Promise.resolve(this.dataHooks.afterDelete(parsed.collectionPath, parsed.entityId, hookCtx)).catch(err => {
|
|
554
|
+
console.error("[BackendHooks] data.afterDelete error:", err instanceof Error ? err.message : err);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
500
558
|
return new Response(null, { status: 204 });
|
|
501
559
|
});
|
|
502
560
|
}
|