@rebasepro/server-core 0.0.1-canary.eae7889 → 0.1.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/app/frontend/node_modules/esbuild/LICENSE.md +21 -0
- package/app/frontend/node_modules/esbuild/README.md +3 -0
- package/app/frontend/node_modules/esbuild/bin/esbuild +220 -0
- package/app/frontend/node_modules/esbuild/install.js +285 -0
- package/app/frontend/node_modules/esbuild/lib/main.d.ts +705 -0
- package/app/frontend/node_modules/esbuild/lib/main.js +2239 -0
- package/app/frontend/node_modules/esbuild/package.json +46 -0
- package/dist/index.es.js +1186 -1673
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1185 -1672
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/rest/api-generator.d.ts +15 -3
- package/dist/server-core/src/auth/admin-routes.d.ts +5 -0
- package/dist/server-core/src/auth/google-oauth.d.ts +36 -3
- package/dist/server-core/src/auth/index.d.ts +1 -0
- package/dist/server-core/src/cron/cron-scheduler.d.ts +45 -0
- package/dist/server-core/src/cron/index.d.ts +1 -1
- package/dist/server-core/src/init.d.ts +11 -1
- package/dist/types/src/controllers/auth.d.ts +8 -2
- package/dist/types/src/controllers/client.d.ts +13 -0
- package/dist/types/src/controllers/collection_registry.d.ts +2 -1
- package/dist/types/src/controllers/data_driver.d.ts +36 -1
- package/dist/types/src/controllers/navigation.d.ts +18 -6
- package/dist/types/src/controllers/registry.d.ts +9 -1
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -0
- package/dist/types/src/rebase_context.d.ts +17 -0
- package/dist/types/src/types/backend_hooks.d.ts +187 -0
- package/dist/types/src/types/collections.d.ts +31 -11
- package/dist/types/src/types/component_ref.d.ts +47 -0
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +6 -7
- package/dist/types/src/types/formex.d.ts +40 -0
- package/dist/types/src/types/index.d.ts +3 -0
- package/dist/types/src/types/plugins.d.ts +6 -3
- package/dist/types/src/types/properties.d.ts +72 -88
- package/dist/types/src/types/slots.d.ts +20 -10
- package/dist/types/src/types/translations.d.ts +6 -0
- package/examples/firebase/node_modules/esbuild/LICENSE.md +21 -0
- package/examples/firebase/node_modules/esbuild/README.md +3 -0
- package/examples/firebase/node_modules/esbuild/bin/esbuild +220 -0
- package/examples/firebase/node_modules/esbuild/install.js +285 -0
- package/examples/firebase/node_modules/esbuild/lib/main.d.ts +705 -0
- package/examples/firebase/node_modules/esbuild/lib/main.js +2239 -0
- package/examples/firebase/node_modules/esbuild/package.json +46 -0
- package/examples/medmot-staging/frontend/node_modules/esbuild/LICENSE.md +21 -0
- package/examples/medmot-staging/frontend/node_modules/esbuild/README.md +3 -0
- package/examples/medmot-staging/frontend/node_modules/esbuild/bin/esbuild +220 -0
- package/examples/medmot-staging/frontend/node_modules/esbuild/install.js +285 -0
- package/examples/medmot-staging/frontend/node_modules/esbuild/lib/main.d.ts +705 -0
- package/examples/medmot-staging/frontend/node_modules/esbuild/lib/main.js +2239 -0
- package/examples/medmot-staging/frontend/node_modules/esbuild/package.json +46 -0
- package/examples/sdk-demo/node_modules/esbuild/LICENSE.md +21 -0
- package/examples/sdk-demo/node_modules/esbuild/README.md +3 -0
- package/examples/sdk-demo/node_modules/esbuild/bin/esbuild +223 -0
- package/examples/sdk-demo/node_modules/esbuild/install.js +289 -0
- package/examples/sdk-demo/node_modules/esbuild/lib/main.d.ts +716 -0
- package/examples/sdk-demo/node_modules/esbuild/lib/main.js +2242 -0
- package/examples/sdk-demo/node_modules/esbuild/package.json +49 -0
- package/package.json +9 -9
- package/packages/client/node_modules/esbuild/LICENSE.md +21 -0
- package/packages/client/node_modules/esbuild/README.md +3 -0
- package/packages/client/node_modules/esbuild/bin/esbuild +220 -0
- package/packages/client/node_modules/esbuild/install.js +285 -0
- package/packages/client/node_modules/esbuild/lib/main.d.ts +705 -0
- package/packages/client/node_modules/esbuild/lib/main.js +2239 -0
- package/packages/client/node_modules/esbuild/package.json +46 -0
- package/packages/client-postgresql/node_modules/esbuild/LICENSE.md +21 -0
- package/packages/client-postgresql/node_modules/esbuild/README.md +3 -0
- package/packages/client-postgresql/node_modules/esbuild/bin/esbuild +220 -0
- package/packages/client-postgresql/node_modules/esbuild/install.js +285 -0
- package/packages/client-postgresql/node_modules/esbuild/lib/main.d.ts +705 -0
- package/packages/client-postgresql/node_modules/esbuild/lib/main.js +2239 -0
- package/packages/client-postgresql/node_modules/esbuild/package.json +46 -0
- package/packages/common/node_modules/esbuild/LICENSE.md +21 -0
- package/packages/common/node_modules/esbuild/README.md +3 -0
- package/packages/common/node_modules/esbuild/bin/esbuild +220 -0
- package/packages/common/node_modules/esbuild/install.js +285 -0
- package/packages/common/node_modules/esbuild/lib/main.d.ts +705 -0
- package/packages/common/node_modules/esbuild/lib/main.js +2239 -0
- package/packages/common/node_modules/esbuild/package.json +46 -0
- package/packages/server-mongodb/node_modules/esbuild/LICENSE.md +21 -0
- package/packages/server-mongodb/node_modules/esbuild/README.md +3 -0
- package/packages/server-mongodb/node_modules/esbuild/bin/esbuild +220 -0
- package/packages/server-mongodb/node_modules/esbuild/install.js +285 -0
- package/packages/server-mongodb/node_modules/esbuild/lib/main.d.ts +705 -0
- package/packages/server-mongodb/node_modules/esbuild/lib/main.js +2239 -0
- package/packages/server-mongodb/node_modules/esbuild/package.json +46 -0
- package/packages/server-postgresql/node_modules/esbuild/LICENSE.md +21 -0
- package/packages/server-postgresql/node_modules/esbuild/README.md +3 -0
- package/packages/server-postgresql/node_modules/esbuild/bin/esbuild +220 -0
- package/packages/server-postgresql/node_modules/esbuild/install.js +285 -0
- package/packages/server-postgresql/node_modules/esbuild/lib/main.d.ts +705 -0
- package/packages/server-postgresql/node_modules/esbuild/lib/main.js +2239 -0
- package/packages/server-postgresql/node_modules/esbuild/package.json +46 -0
- package/packages/types/node_modules/esbuild/LICENSE.md +21 -0
- package/packages/types/node_modules/esbuild/README.md +3 -0
- package/packages/types/node_modules/esbuild/bin/esbuild +220 -0
- package/packages/types/node_modules/esbuild/install.js +285 -0
- package/packages/types/node_modules/esbuild/lib/main.d.ts +705 -0
- package/packages/types/node_modules/esbuild/lib/main.js +2239 -0
- package/packages/types/node_modules/esbuild/package.json +46 -0
- package/packages/utils/node_modules/esbuild/LICENSE.md +21 -0
- package/packages/utils/node_modules/esbuild/README.md +3 -0
- package/packages/utils/node_modules/esbuild/bin/esbuild +220 -0
- package/packages/utils/node_modules/esbuild/install.js +285 -0
- package/packages/utils/node_modules/esbuild/lib/main.d.ts +705 -0
- package/packages/utils/node_modules/esbuild/lib/main.js +2239 -0
- package/packages/utils/node_modules/esbuild/package.json +46 -0
- package/src/api/errors.ts +3 -2
- package/src/api/rest/api-generator-count.test.ts +113 -0
- package/src/api/rest/api-generator.ts +123 -22
- package/src/api/server.ts +8 -4
- package/src/auth/admin-routes.ts +133 -57
- package/src/auth/apple-oauth.ts +8 -18
- package/src/auth/google-oauth.ts +192 -22
- package/src/auth/index.ts +1 -0
- package/src/auth/rate-limiter.ts +9 -5
- package/src/auth/routes.ts +25 -5
- package/src/collections/loader.ts +3 -3
- package/src/cron/cron-scheduler.test.ts +301 -175
- package/src/cron/cron-scheduler.ts +220 -57
- package/src/cron/index.ts +1 -1
- package/src/init.ts +27 -5
- package/src/storage/LocalStorageController.ts +37 -13
- package/src/storage/S3StorageController.ts +4 -1
- package/src/storage/routes.ts +51 -5
- package/test/backend-hooks-admin.test.ts +394 -0
- package/test/backend-hooks-data.test.ts +408 -0
- package/history_diff.log +0 -385
- package/scratch.ts +0 -9
- package/test-ast.ts +0 -28
- package/test_output.txt +0 -1133
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "esbuild",
|
|
3
|
+
"version": "0.21.5",
|
|
4
|
+
"description": "An extremely fast JavaScript and CSS bundler and minifier.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/evanw/esbuild.git"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"postinstall": "node install.js"
|
|
11
|
+
},
|
|
12
|
+
"main": "lib/main.js",
|
|
13
|
+
"types": "lib/main.d.ts",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=12"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"esbuild": "bin/esbuild"
|
|
19
|
+
},
|
|
20
|
+
"optionalDependencies": {
|
|
21
|
+
"@esbuild/aix-ppc64": "0.21.5",
|
|
22
|
+
"@esbuild/android-arm": "0.21.5",
|
|
23
|
+
"@esbuild/android-arm64": "0.21.5",
|
|
24
|
+
"@esbuild/android-x64": "0.21.5",
|
|
25
|
+
"@esbuild/darwin-arm64": "0.21.5",
|
|
26
|
+
"@esbuild/darwin-x64": "0.21.5",
|
|
27
|
+
"@esbuild/freebsd-arm64": "0.21.5",
|
|
28
|
+
"@esbuild/freebsd-x64": "0.21.5",
|
|
29
|
+
"@esbuild/linux-arm": "0.21.5",
|
|
30
|
+
"@esbuild/linux-arm64": "0.21.5",
|
|
31
|
+
"@esbuild/linux-ia32": "0.21.5",
|
|
32
|
+
"@esbuild/linux-loong64": "0.21.5",
|
|
33
|
+
"@esbuild/linux-mips64el": "0.21.5",
|
|
34
|
+
"@esbuild/linux-ppc64": "0.21.5",
|
|
35
|
+
"@esbuild/linux-riscv64": "0.21.5",
|
|
36
|
+
"@esbuild/linux-s390x": "0.21.5",
|
|
37
|
+
"@esbuild/linux-x64": "0.21.5",
|
|
38
|
+
"@esbuild/netbsd-x64": "0.21.5",
|
|
39
|
+
"@esbuild/openbsd-x64": "0.21.5",
|
|
40
|
+
"@esbuild/sunos-x64": "0.21.5",
|
|
41
|
+
"@esbuild/win32-arm64": "0.21.5",
|
|
42
|
+
"@esbuild/win32-ia32": "0.21.5",
|
|
43
|
+
"@esbuild/win32-x64": "0.21.5"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT"
|
|
46
|
+
}
|
package/src/api/errors.ts
CHANGED
|
@@ -120,10 +120,11 @@ export const errorHandler: ErrorHandler = (err, c) => {
|
|
|
120
120
|
`❌ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}`
|
|
121
121
|
);
|
|
122
122
|
|
|
123
|
-
// Suppress the huge stack trace for known
|
|
123
|
+
// Suppress the huge stack trace for known DB errors (it's noisy and leaks SQL)
|
|
124
124
|
const causePg = (error.cause && typeof error.cause === "object") ? (error.cause as PgLikeError) : undefined;
|
|
125
125
|
const pgErrorCode = causePg?.code || error.code;
|
|
126
|
-
|
|
126
|
+
const suppressStack = pgErrorCode === "42703" || pgErrorCode === "42P01" || (statusCode < 500 && code === "BAD_REQUEST");
|
|
127
|
+
if (!suppressStack) {
|
|
127
128
|
console.error(error.stack || error);
|
|
128
129
|
}
|
|
129
130
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { RestApiGenerator } from "./api-generator";
|
|
3
|
+
import type { DataDriver, EntityCollection, FetchCollectionProps } from "@rebasepro/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal mock DataDriver for testing.
|
|
7
|
+
*/
|
|
8
|
+
function createMockDriver(overrides?: Partial<DataDriver>): DataDriver {
|
|
9
|
+
return {
|
|
10
|
+
fetchCollection: jest.fn().mockResolvedValue([]),
|
|
11
|
+
fetchEntity: jest.fn().mockResolvedValue(null),
|
|
12
|
+
saveEntity: jest.fn().mockResolvedValue({ id: "1", path: "test", values: {} }),
|
|
13
|
+
deleteEntity: jest.fn().mockResolvedValue(undefined),
|
|
14
|
+
countEntities: jest.fn().mockResolvedValue(0),
|
|
15
|
+
...overrides,
|
|
16
|
+
} as unknown as DataDriver;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createTestCollection(slug: string): EntityCollection {
|
|
20
|
+
return {
|
|
21
|
+
slug,
|
|
22
|
+
name: slug.charAt(0).toUpperCase() + slug.slice(1),
|
|
23
|
+
path: slug,
|
|
24
|
+
properties: {},
|
|
25
|
+
} as unknown as EntityCollection;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("RestApiGenerator - Count Endpoint", () => {
|
|
29
|
+
let driver: DataDriver;
|
|
30
|
+
let collection: EntityCollection;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
driver = createMockDriver({
|
|
34
|
+
countEntities: jest.fn().mockResolvedValue(42),
|
|
35
|
+
});
|
|
36
|
+
collection = createTestCollection("products");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("GET /products/count should return a count object", async () => {
|
|
40
|
+
const generator = new RestApiGenerator([collection], driver);
|
|
41
|
+
const app = generator.generateRoutes();
|
|
42
|
+
|
|
43
|
+
const res = await app.request("/products/count");
|
|
44
|
+
expect(res.status).toBe(200);
|
|
45
|
+
|
|
46
|
+
const json = await res.json() as { count: number };
|
|
47
|
+
expect(json.count).toBe(42);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should pass filters to countEntities driver", async () => {
|
|
51
|
+
const generator = new RestApiGenerator([collection], driver);
|
|
52
|
+
const app = generator.generateRoutes();
|
|
53
|
+
|
|
54
|
+
const res = await app.request("/products/count?status=eq.active");
|
|
55
|
+
expect(res.status).toBe(200);
|
|
56
|
+
|
|
57
|
+
const json = await res.json() as { count: number };
|
|
58
|
+
expect(json.count).toBe(42);
|
|
59
|
+
|
|
60
|
+
// Verify countEntities was called with the filter
|
|
61
|
+
expect(driver.countEntities).toHaveBeenCalled();
|
|
62
|
+
const callArgs = (driver.countEntities as ReturnType<typeof jest.fn>).mock.calls[0][0] as FetchCollectionProps;
|
|
63
|
+
expect(callArgs.path).toBe("products");
|
|
64
|
+
expect(callArgs.filter).toHaveProperty("status");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should pass searchString to countEntities driver", async () => {
|
|
68
|
+
const generator = new RestApiGenerator([collection], driver);
|
|
69
|
+
const app = generator.generateRoutes();
|
|
70
|
+
|
|
71
|
+
const res = await app.request("/products/count?searchString=widget");
|
|
72
|
+
expect(res.status).toBe(200);
|
|
73
|
+
|
|
74
|
+
const json = await res.json() as { count: number };
|
|
75
|
+
expect(json.count).toBe(42);
|
|
76
|
+
|
|
77
|
+
expect(driver.countEntities).toHaveBeenCalled();
|
|
78
|
+
const callArgs = (driver.countEntities as ReturnType<typeof jest.fn>).mock.calls[0][0] as FetchCollectionProps;
|
|
79
|
+
expect(callArgs.searchString).toBe("widget");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should return 0 when countEntities is not available on driver", async () => {
|
|
83
|
+
const driverWithoutCount = createMockDriver({ countEntities: undefined });
|
|
84
|
+
const generator = new RestApiGenerator([collection], driverWithoutCount);
|
|
85
|
+
const app = generator.generateRoutes();
|
|
86
|
+
|
|
87
|
+
const res = await app.request("/products/count");
|
|
88
|
+
expect(res.status).toBe(200);
|
|
89
|
+
|
|
90
|
+
const json = await res.json() as { count: number };
|
|
91
|
+
expect(json.count).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("GET /products/count should not be confused with GET /products/:id", async () => {
|
|
95
|
+
// Ensure the count route is registered before the :id route
|
|
96
|
+
const fetchEntity = jest.fn().mockResolvedValue(null);
|
|
97
|
+
const driverCustom = createMockDriver({
|
|
98
|
+
countEntities: jest.fn().mockResolvedValue(99),
|
|
99
|
+
fetchEntity,
|
|
100
|
+
});
|
|
101
|
+
const generator = new RestApiGenerator([collection], driverCustom);
|
|
102
|
+
const app = generator.generateRoutes();
|
|
103
|
+
|
|
104
|
+
const res = await app.request("/products/count");
|
|
105
|
+
expect(res.status).toBe(200);
|
|
106
|
+
|
|
107
|
+
const json = await res.json() as { count: number };
|
|
108
|
+
expect(json.count).toBe(99);
|
|
109
|
+
|
|
110
|
+
// fetchEntity should NOT have been called (i.e. "count" was not treated as an entity ID)
|
|
111
|
+
expect(fetchEntity).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import { DataDriver, Entity, EntityCollection, FetchCollectionProps } from "@rebasepro/types";
|
|
2
|
+
import { DataDriver, Entity, EntityCollection, FetchCollectionProps, DataHooks, BackendHookContext, RestFetchService } from "@rebasepro/types";
|
|
3
3
|
import { QueryOptions, HonoEnv } from "../types";
|
|
4
4
|
import { ApiError } from "../errors";
|
|
5
5
|
import { parseQueryOptions } from "./query-parser";
|
|
@@ -13,13 +13,24 @@ export class RestApiGenerator {
|
|
|
13
13
|
private collections: EntityCollection[];
|
|
14
14
|
private router: Hono<HonoEnv>;
|
|
15
15
|
private driver: DataDriver;
|
|
16
|
+
private dataHooks?: DataHooks;
|
|
16
17
|
|
|
17
|
-
constructor(collections: EntityCollection[], driver: DataDriver) {
|
|
18
|
+
constructor(collections: EntityCollection[], driver: DataDriver, dataHooks?: DataHooks) {
|
|
18
19
|
this.collections = collections;
|
|
19
20
|
this.driver = driver;
|
|
21
|
+
this.dataHooks = dataHooks;
|
|
20
22
|
this.router = new Hono<HonoEnv>();
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
/** Build a BackendHookContext from a Hono context */
|
|
26
|
+
private buildHookContext(c: { get: (key: string) => unknown }, method: BackendHookContext["method"]): BackendHookContext {
|
|
27
|
+
const user = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
28
|
+
return {
|
|
29
|
+
requestUser: user ? { userId: user.userId, roles: user.roles ?? [] } : undefined,
|
|
30
|
+
method
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
/**
|
|
24
35
|
* Generate REST routes using existing DataDriver
|
|
25
36
|
*/
|
|
@@ -37,16 +48,10 @@ export class RestApiGenerator {
|
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
/**
|
|
40
|
-
* Get the
|
|
51
|
+
* Get the typed RestFetchService from a driver if it exposes one (for include support).
|
|
41
52
|
*/
|
|
42
|
-
private getFetchService(driver: DataDriver):
|
|
43
|
-
|
|
44
|
-
const es = driver.entityService as Record<string, unknown>;
|
|
45
|
-
if (typeof es.getFetchService === "function") {
|
|
46
|
-
return es.getFetchService();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return null;
|
|
53
|
+
private getFetchService(driver: DataDriver): RestFetchService | undefined {
|
|
54
|
+
return driver.restFetchService;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
/**
|
|
@@ -56,6 +61,17 @@ export class RestApiGenerator {
|
|
|
56
61
|
const basePath = `/${collection.slug}`;
|
|
57
62
|
const resolvedCollection = collection;
|
|
58
63
|
|
|
64
|
+
// GET /collection/count - Count entities (with optional filters)
|
|
65
|
+
this.router.get(`${basePath}/count`, async (c) => {
|
|
66
|
+
const queryDict = c.req.query();
|
|
67
|
+
const queryOptions = parseQueryOptions(queryDict);
|
|
68
|
+
const searchString = queryDict.searchString as string | undefined;
|
|
69
|
+
const driver = c.get("driver") || this.driver;
|
|
70
|
+
|
|
71
|
+
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
|
|
72
|
+
return c.json({ count: total });
|
|
73
|
+
});
|
|
74
|
+
|
|
59
75
|
// GET /collection - List entities
|
|
60
76
|
this.router.get(basePath, async (c) => {
|
|
61
77
|
const queryDict = c.req.query();
|
|
@@ -64,11 +80,12 @@ export class RestApiGenerator {
|
|
|
64
80
|
|
|
65
81
|
const driver = c.get("driver") || this.driver;
|
|
66
82
|
const fetchService = this.getFetchService(driver);
|
|
83
|
+
const hookCtx = this.buildHookContext(c, "GET");
|
|
67
84
|
|
|
68
85
|
// Use include-aware path when available
|
|
69
86
|
if (fetchService) {
|
|
70
87
|
const collectionPath = collection.slug;
|
|
71
|
-
|
|
88
|
+
let entities = await fetchService.fetchCollectionForRest(
|
|
72
89
|
collectionPath,
|
|
73
90
|
{
|
|
74
91
|
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
@@ -81,6 +98,8 @@ export class RestApiGenerator {
|
|
|
81
98
|
queryOptions.include
|
|
82
99
|
);
|
|
83
100
|
|
|
101
|
+
entities = await this.applyAfterReadBatch(collection.slug, entities, hookCtx);
|
|
102
|
+
|
|
84
103
|
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
|
|
85
104
|
|
|
86
105
|
return c.json({
|
|
@@ -89,13 +108,15 @@ export class RestApiGenerator {
|
|
|
89
108
|
total,
|
|
90
109
|
limit: queryOptions.limit,
|
|
91
110
|
offset: queryOptions.offset,
|
|
92
|
-
hasMore: (queryOptions.offset || 0) +
|
|
111
|
+
hasMore: (queryOptions.offset || 0) + entities.length < total
|
|
93
112
|
}
|
|
94
113
|
});
|
|
95
114
|
}
|
|
96
115
|
|
|
97
116
|
// Fallback path
|
|
98
|
-
|
|
117
|
+
let entities = await this.fetchRawCollection(driver, resolvedCollection, queryOptions, searchString);
|
|
118
|
+
|
|
119
|
+
entities = await this.applyAfterReadBatch(collection.slug, entities, hookCtx);
|
|
99
120
|
|
|
100
121
|
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
|
|
101
122
|
|
|
@@ -105,7 +126,7 @@ export class RestApiGenerator {
|
|
|
105
126
|
total,
|
|
106
127
|
limit: queryOptions.limit,
|
|
107
128
|
offset: queryOptions.offset,
|
|
108
|
-
hasMore: (queryOptions.offset || 0) +
|
|
129
|
+
hasMore: (queryOptions.offset || 0) + entities.length < total
|
|
109
130
|
}
|
|
110
131
|
});
|
|
111
132
|
});
|
|
@@ -117,11 +138,12 @@ export class RestApiGenerator {
|
|
|
117
138
|
const queryOptions = parseQueryOptions(queryDict);
|
|
118
139
|
const driver = c.get("driver") || this.driver;
|
|
119
140
|
const fetchService = this.getFetchService(driver);
|
|
141
|
+
const hookCtx = this.buildHookContext(c, "GET");
|
|
120
142
|
|
|
121
143
|
// Use include-aware path when available
|
|
122
144
|
if (fetchService) {
|
|
123
145
|
const collectionPath = collection.slug;
|
|
124
|
-
|
|
146
|
+
let entity = await fetchService.fetchEntityForRest(
|
|
125
147
|
collectionPath,
|
|
126
148
|
String(id),
|
|
127
149
|
queryOptions.include
|
|
@@ -131,16 +153,26 @@ export class RestApiGenerator {
|
|
|
131
153
|
throw ApiError.notFound("Entity not found");
|
|
132
154
|
}
|
|
133
155
|
|
|
156
|
+
entity = await this.applyAfterRead(collection.slug, entity, hookCtx);
|
|
157
|
+
if (!entity) {
|
|
158
|
+
throw ApiError.notFound("Entity not found");
|
|
159
|
+
}
|
|
160
|
+
|
|
134
161
|
return c.json(entity);
|
|
135
162
|
}
|
|
136
163
|
|
|
137
164
|
// Fallback
|
|
138
|
-
|
|
165
|
+
let entity = await this.fetchRawEntity(driver, resolvedCollection, String(id));
|
|
139
166
|
|
|
140
167
|
if (!entity) {
|
|
141
168
|
throw ApiError.notFound("Entity not found");
|
|
142
169
|
}
|
|
143
170
|
|
|
171
|
+
entity = await this.applyAfterRead(collection.slug, entity, hookCtx);
|
|
172
|
+
if (!entity) {
|
|
173
|
+
throw ApiError.notFound("Entity not found");
|
|
174
|
+
}
|
|
175
|
+
|
|
144
176
|
return c.json(entity);
|
|
145
177
|
});
|
|
146
178
|
|
|
@@ -149,8 +181,13 @@ export class RestApiGenerator {
|
|
|
149
181
|
try {
|
|
150
182
|
const driver = c.get("driver") || this.driver;
|
|
151
183
|
const path = collection.slug;
|
|
184
|
+
const hookCtx = this.buildHookContext(c, "POST");
|
|
152
185
|
|
|
153
|
-
|
|
186
|
+
let body = await c.req.json().catch(() => ({}));
|
|
187
|
+
|
|
188
|
+
if (this.dataHooks?.beforeSave) {
|
|
189
|
+
body = await this.dataHooks.beforeSave(path, body, undefined, hookCtx);
|
|
190
|
+
}
|
|
154
191
|
|
|
155
192
|
const entity = await driver.saveEntity({
|
|
156
193
|
path,
|
|
@@ -159,7 +196,15 @@ export class RestApiGenerator {
|
|
|
159
196
|
status: "new"
|
|
160
197
|
});
|
|
161
198
|
|
|
162
|
-
|
|
199
|
+
const response = this.formatResponse(entity);
|
|
200
|
+
|
|
201
|
+
if (this.dataHooks?.afterSave) {
|
|
202
|
+
Promise.resolve(this.dataHooks.afterSave(path, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
203
|
+
console.error("[BackendHooks] data.afterSave error:", err instanceof Error ? err.message : err);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return c.json(response, 201);
|
|
163
208
|
} catch (error) {
|
|
164
209
|
const err = error as Error & { code?: string };
|
|
165
210
|
err.code = err.code || "BAD_REQUEST";
|
|
@@ -172,6 +217,7 @@ export class RestApiGenerator {
|
|
|
172
217
|
try {
|
|
173
218
|
const id = c.req.param("id");
|
|
174
219
|
const driver = c.get("driver") || this.driver;
|
|
220
|
+
const hookCtx = this.buildHookContext(c, "PUT");
|
|
175
221
|
|
|
176
222
|
const existingEntity = await driver.fetchEntity({
|
|
177
223
|
path: collection.slug,
|
|
@@ -183,7 +229,11 @@ export class RestApiGenerator {
|
|
|
183
229
|
throw ApiError.notFound("Entity not found");
|
|
184
230
|
}
|
|
185
231
|
|
|
186
|
-
|
|
232
|
+
let body = await c.req.json().catch(() => ({}));
|
|
233
|
+
|
|
234
|
+
if (this.dataHooks?.beforeSave) {
|
|
235
|
+
body = await this.dataHooks.beforeSave(collection.slug, body, String(id), hookCtx);
|
|
236
|
+
}
|
|
187
237
|
|
|
188
238
|
const entity = await driver.saveEntity({
|
|
189
239
|
path: collection.slug,
|
|
@@ -193,7 +243,15 @@ export class RestApiGenerator {
|
|
|
193
243
|
status: "existing"
|
|
194
244
|
});
|
|
195
245
|
|
|
196
|
-
|
|
246
|
+
const response = this.formatResponse(entity);
|
|
247
|
+
|
|
248
|
+
if (this.dataHooks?.afterSave) {
|
|
249
|
+
Promise.resolve(this.dataHooks.afterSave(collection.slug, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
250
|
+
console.error("[BackendHooks] data.afterSave error:", err instanceof Error ? err.message : err);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return c.json(response);
|
|
197
255
|
} catch (error) {
|
|
198
256
|
const err = error as Error & { code?: string };
|
|
199
257
|
err.code = err.code || "BAD_REQUEST";
|
|
@@ -205,6 +263,7 @@ export class RestApiGenerator {
|
|
|
205
263
|
this.router.delete(`${basePath}/:id`, async (c) => {
|
|
206
264
|
const id = c.req.param("id");
|
|
207
265
|
const driver = c.get("driver") || this.driver;
|
|
266
|
+
const hookCtx = this.buildHookContext(c, "DELETE");
|
|
208
267
|
|
|
209
268
|
const existingEntity = await driver.fetchEntity({
|
|
210
269
|
path: collection.slug,
|
|
@@ -216,11 +275,21 @@ export class RestApiGenerator {
|
|
|
216
275
|
throw ApiError.notFound("Entity not found");
|
|
217
276
|
}
|
|
218
277
|
|
|
278
|
+
if (this.dataHooks?.beforeDelete) {
|
|
279
|
+
await this.dataHooks.beforeDelete(collection.slug, String(id), hookCtx);
|
|
280
|
+
}
|
|
281
|
+
|
|
219
282
|
await driver.deleteEntity({
|
|
220
283
|
entity: existingEntity,
|
|
221
284
|
collection: resolvedCollection
|
|
222
285
|
});
|
|
223
286
|
|
|
287
|
+
if (this.dataHooks?.afterDelete) {
|
|
288
|
+
Promise.resolve(this.dataHooks.afterDelete(collection.slug, String(id), hookCtx)).catch(err => {
|
|
289
|
+
console.error("[BackendHooks] data.afterDelete error:", err instanceof Error ? err.message : err);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
224
293
|
return new Response(null, { status: 204 });
|
|
225
294
|
});
|
|
226
295
|
}
|
|
@@ -285,7 +354,19 @@ entityId };
|
|
|
285
354
|
|
|
286
355
|
const driver = c.get("driver") || this.driver;
|
|
287
356
|
|
|
288
|
-
if (parsed.entityId) {
|
|
357
|
+
if (parsed.entityId === "count") {
|
|
358
|
+
// GET /parent/:parentId/child/count — count child entities
|
|
359
|
+
const queryDict = c.req.query();
|
|
360
|
+
const queryOptions = parseQueryOptions(queryDict);
|
|
361
|
+
|
|
362
|
+
const total = driver.countEntities ? await driver.countEntities({
|
|
363
|
+
path: parsed.collectionPath,
|
|
364
|
+
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
365
|
+
searchString: queryDict.searchString as string | undefined
|
|
366
|
+
}) : 0;
|
|
367
|
+
|
|
368
|
+
return c.json({ count: total });
|
|
369
|
+
} else if (parsed.entityId) {
|
|
289
370
|
// GET /parent/:parentId/child/:id — single entity
|
|
290
371
|
const entity = await driver.fetchEntity({
|
|
291
372
|
path: parsed.collectionPath,
|
|
@@ -469,4 +550,24 @@ entityId };
|
|
|
469
550
|
|
|
470
551
|
return entity ? this.flattenEntity(entity) : null;
|
|
471
552
|
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Apply data.afterRead hook to a single entity.
|
|
556
|
+
* Returns the transformed entity, or null to filter it out.
|
|
557
|
+
*/
|
|
558
|
+
private async applyAfterRead(slug: string, entity: Record<string, unknown>, ctx: BackendHookContext): Promise<Record<string, unknown> | null> {
|
|
559
|
+
if (!this.dataHooks?.afterRead) return entity;
|
|
560
|
+
return this.dataHooks.afterRead(slug, entity, ctx);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Apply data.afterRead hook to an array of entities, filtering out nulls.
|
|
565
|
+
*/
|
|
566
|
+
private async applyAfterReadBatch(slug: string, entities: Record<string, unknown>[], ctx: BackendHookContext): Promise<Record<string, unknown>[]> {
|
|
567
|
+
if (!this.dataHooks?.afterRead) return entities;
|
|
568
|
+
const results = await Promise.all(
|
|
569
|
+
entities.map(e => this.applyAfterRead(slug, e, ctx))
|
|
570
|
+
);
|
|
571
|
+
return results.filter((e): e is Record<string, unknown> => e !== null);
|
|
572
|
+
}
|
|
472
573
|
}
|
package/src/api/server.ts
CHANGED
|
@@ -69,8 +69,11 @@ export class RebaseApiServer {
|
|
|
69
69
|
* Setup Hono middleware
|
|
70
70
|
*/
|
|
71
71
|
private setupMiddleware(): void {
|
|
72
|
-
// Security headers
|
|
73
|
-
|
|
72
|
+
// Security headers — use same-origin-allow-popups for COOP so that
|
|
73
|
+
// OAuth popup flows (Google, etc.) can postMessage back to the opener.
|
|
74
|
+
this.router.use("/*", secureHeaders({
|
|
75
|
+
crossOriginOpenerPolicy: "same-origin-allow-popups"
|
|
76
|
+
}));
|
|
74
77
|
|
|
75
78
|
// CORS — only applied if explicitly configured via `cors` option.
|
|
76
79
|
// If omitted, the user is expected to configure CORS on their own
|
|
@@ -178,10 +181,11 @@ export class RebaseApiServer {
|
|
|
178
181
|
if (process.env.NODE_ENV === "production") {
|
|
179
182
|
console.warn("[RebaseApiServer] Schema Editor is disabled in production environments for security.");
|
|
180
183
|
} else {
|
|
181
|
-
const schemaEditorRoutes = createSchemaEditorRoutes(this.config.collectionsDir);
|
|
182
|
-
this.router.route(`${basePath}/schema-editor`, schemaEditorRoutes);
|
|
183
184
|
// Auth middlewares applied to schema-editor via the router prefix
|
|
185
|
+
// MUST be declared before .route() so they execute first
|
|
184
186
|
this.router.use(`${basePath}/schema-editor/*`, requireAuth, requireAdmin);
|
|
187
|
+
const schemaEditorRoutes = createSchemaEditorRoutes(this.config.collectionsDir);
|
|
188
|
+
this.router.route(`${basePath}/schema-editor`, schemaEditorRoutes);
|
|
185
189
|
}
|
|
186
190
|
}
|
|
187
191
|
|