@rebasepro/server-core 0.6.1 → 0.8.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/dist/api/graphql/graphql-schema-generator.d.ts +9 -0
- package/dist/api/rest/api-generator.d.ts +2 -14
- package/dist/api/rest/query-parser.d.ts +2 -5
- package/dist/api/types.d.ts +8 -2
- package/dist/auth/adapter-middleware.d.ts +7 -1
- package/dist/auth/admin-roles-route.d.ts +18 -0
- package/dist/auth/admin-users-route.d.ts +28 -0
- package/dist/auth/api-keys/api-key-types.d.ts +8 -0
- package/dist/auth/auth-hooks.d.ts +17 -0
- package/dist/auth/builtin-auth-adapter.d.ts +3 -1
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/interfaces.d.ts +32 -2
- package/dist/auth/magic-link-routes.d.ts +30 -0
- package/dist/auth/mfa-crypto.d.ts +23 -0
- package/dist/auth/mfa-routes.d.ts +2 -1
- package/dist/auth/middleware.d.ts +9 -2
- package/dist/auth/routes.d.ts +3 -1
- package/dist/auth/session-routes.d.ts +2 -0
- package/dist/backend-CIxN4FVm.js.map +1 -1
- package/dist/base64-js-C_frYBkI.js +1687 -0
- package/dist/base64-js-C_frYBkI.js.map +1 -0
- package/dist/{dist-CZKP-Xz4.js → dist-DMO-zF6D.js} +2 -2
- package/dist/{dist-CZKP-Xz4.js.map → dist-DMO-zF6D.js.map} +1 -1
- package/dist/email/index.d.ts +2 -2
- package/dist/email/templates.d.ts +8 -0
- package/dist/email/types.d.ts +18 -0
- package/dist/functions/define-function.d.ts +50 -0
- package/dist/functions/index.d.ts +2 -0
- package/dist/index.d.ts +16 -18
- package/dist/index.es.js +1947 -2356
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +40615 -10203
- package/dist/index.umd.js.map +1 -1
- package/dist/init.d.ts +68 -12
- package/dist/jws-BqRRaK11.js +616 -0
- package/dist/jws-BqRRaK11.js.map +1 -0
- package/dist/{jwt-DHcQRGC3.js → jwt-BETC8a1J.js} +3 -611
- package/dist/jwt-BETC8a1J.js.map +1 -0
- package/dist/services/routed-realtime-service.d.ts +43 -0
- package/dist/{src-COaj0G3P.js → src-CB2PIpBe.js} +2 -2
- package/dist/{src-COaj0G3P.js.map → src-CB2PIpBe.js.map} +1 -1
- package/dist/src-DjzOT1kG.js +29746 -0
- package/dist/src-DjzOT1kG.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +43 -0
- package/dist/storage/index.d.ts +5 -2
- package/dist/storage/routes.d.ts +33 -1
- package/dist/storage/tus-handler.d.ts +3 -1
- package/dist/storage/types.d.ts +28 -3
- package/dist/utils/dev-port.d.ts +2 -0
- package/package.json +13 -5
- package/src/api/errors.ts +16 -2
- package/src/api/graphql/graphql-schema-generator.ts +50 -15
- package/src/api/openapi-generator.ts +2 -2
- package/src/api/rest/api-generator.ts +44 -123
- package/src/api/rest/query-parser.ts +6 -23
- package/src/api/server.ts +10 -2
- package/src/api/types.ts +8 -2
- package/src/auth/adapter-middleware.ts +10 -2
- package/src/auth/admin-roles-route.ts +36 -0
- package/src/auth/admin-users-route.ts +302 -0
- package/src/auth/api-keys/api-key-middleware.ts +4 -3
- package/src/auth/api-keys/api-key-routes.ts +12 -2
- package/src/auth/api-keys/api-key-store.ts +83 -66
- package/src/auth/api-keys/api-key-types.ts +8 -0
- package/src/auth/apple-oauth.ts +2 -1
- package/src/auth/auth-hooks.ts +21 -0
- package/src/auth/bitbucket-oauth.ts +2 -1
- package/src/auth/builtin-auth-adapter.ts +29 -6
- package/src/auth/custom-auth-adapter.ts +2 -0
- package/src/auth/discord-oauth.ts +2 -1
- package/src/auth/facebook-oauth.ts +2 -1
- package/src/auth/github-oauth.ts +2 -1
- package/src/auth/gitlab-oauth.ts +2 -1
- package/src/auth/google-oauth.ts +8 -4
- package/src/auth/index.ts +2 -0
- package/src/auth/interfaces.ts +38 -2
- package/src/auth/linkedin-oauth.ts +2 -1
- package/src/auth/magic-link-routes.ts +167 -0
- package/src/auth/mfa-crypto.ts +91 -0
- package/src/auth/mfa-routes.ts +34 -10
- package/src/auth/microsoft-oauth.ts +2 -1
- package/src/auth/middleware.ts +14 -5
- package/src/auth/reset-password-admin.ts +17 -1
- package/src/auth/routes.ts +78 -10
- package/src/auth/session-routes.ts +15 -3
- package/src/auth/slack-oauth.ts +2 -1
- package/src/auth/spotify-oauth.ts +2 -1
- package/src/auth/twitter-oauth.ts +8 -1
- package/src/cron/cron-store.ts +25 -23
- package/src/email/index.ts +3 -2
- package/src/email/templates.ts +82 -0
- package/src/email/types.ts +16 -0
- package/src/functions/define-function.ts +59 -0
- package/src/functions/function-loader.ts +16 -0
- package/src/functions/index.ts +2 -0
- package/src/index.ts +70 -37
- package/src/init.ts +214 -25
- package/src/services/routed-realtime-service.ts +113 -0
- package/src/storage/GCSStorageController.ts +334 -0
- package/src/storage/index.ts +9 -3
- package/src/storage/routes.ts +191 -23
- package/src/storage/tus-handler.ts +16 -4
- package/src/storage/types.ts +25 -3
- package/src/utils/dev-port.ts +13 -7
- package/test/api-generator.test.ts +1 -1
- package/test/auth-config-types.test.ts +40 -0
- package/test/auth-routes.test.ts +54 -4
- package/test/custom-auth-adapter.test.ts +20 -1
- package/test/define-function.test.ts +45 -0
- package/test/env.test.ts +9 -19
- package/test/multi-datasource-routing.test.ts +113 -0
- package/test/routed-realtime-service.test.ts +86 -0
- package/test/storage-routes.test.ts +160 -0
- package/test/transform-auth-response.test.ts +305 -0
- package/dist/jwt-DHcQRGC3.js.map +0 -1
- package/test/backend-hooks-data.test.ts +0 -477
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import { AuthAdapter, DataDriver, Entity, EntityCollection,
|
|
2
|
+
import { AuthAdapter, DataDriver, Entity, EntityCollection, RestFetchService, getCollectionDataPath } from "@rebasepro/types";
|
|
3
3
|
import { QueryOptions, HonoEnv } from "../types";
|
|
4
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";
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -16,31 +16,21 @@ export class RestApiGenerator {
|
|
|
16
16
|
private collections: EntityCollection[];
|
|
17
17
|
private router: Hono<HonoEnv>;
|
|
18
18
|
private driver: DataDriver;
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
private authAdapter?: AuthAdapter;
|
|
21
21
|
|
|
22
22
|
constructor(
|
|
23
23
|
collections: EntityCollection[],
|
|
24
24
|
driver: DataDriver,
|
|
25
|
-
dataHooks?: DataHooks,
|
|
26
25
|
authAdapter?: AuthAdapter
|
|
27
26
|
) {
|
|
28
27
|
this.collections = collections;
|
|
29
28
|
this.driver = driver;
|
|
30
|
-
this.dataHooks = dataHooks;
|
|
31
29
|
this.authAdapter = authAdapter;
|
|
32
30
|
this.router = new Hono<HonoEnv>();
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
private buildHookContext(c: { get: (key: string) => unknown }, method: BackendHookContext["method"]): BackendHookContext {
|
|
37
|
-
const user = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
38
|
-
return {
|
|
39
|
-
requestUser: user ? { userId: user.userId,
|
|
40
|
-
roles: user.roles ?? [] } : undefined,
|
|
41
|
-
method
|
|
42
|
-
};
|
|
43
|
-
}
|
|
33
|
+
|
|
44
34
|
|
|
45
35
|
/**
|
|
46
36
|
* Generate REST routes using existing DataDriver
|
|
@@ -124,7 +114,7 @@ roles: user.roles ?? [] } : undefined,
|
|
|
124
114
|
|
|
125
115
|
const driver = this.getScopedDriver(c);
|
|
126
116
|
const fetchService = this.getFetchService(driver);
|
|
127
|
-
|
|
117
|
+
|
|
128
118
|
|
|
129
119
|
// Use include-aware path when available
|
|
130
120
|
if (fetchService) {
|
|
@@ -132,7 +122,7 @@ roles: user.roles ?? [] } : undefined,
|
|
|
132
122
|
let entities = await fetchService.fetchCollectionForRest(
|
|
133
123
|
collectionPath,
|
|
134
124
|
{
|
|
135
|
-
filter: queryOptions.where
|
|
125
|
+
filter: queryOptions.where,
|
|
136
126
|
limit: queryOptions.limit,
|
|
137
127
|
offset: queryOptions.offset,
|
|
138
128
|
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
@@ -143,7 +133,7 @@ roles: user.roles ?? [] } : undefined,
|
|
|
143
133
|
queryOptions.include
|
|
144
134
|
);
|
|
145
135
|
|
|
146
|
-
|
|
136
|
+
|
|
147
137
|
|
|
148
138
|
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
|
|
149
139
|
|
|
@@ -161,7 +151,7 @@ roles: user.roles ?? [] } : undefined,
|
|
|
161
151
|
// Fallback path
|
|
162
152
|
let entities = await this.fetchRawCollection(driver, resolvedCollection, queryOptions, searchString);
|
|
163
153
|
|
|
164
|
-
|
|
154
|
+
|
|
165
155
|
|
|
166
156
|
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
|
|
167
157
|
|
|
@@ -184,7 +174,7 @@ roles: user.roles ?? [] } : undefined,
|
|
|
184
174
|
const queryOptions = parseQueryOptions(queryDict);
|
|
185
175
|
const driver = this.getScopedDriver(c);
|
|
186
176
|
const fetchService = this.getFetchService(driver);
|
|
187
|
-
|
|
177
|
+
|
|
188
178
|
|
|
189
179
|
// Use include-aware path when available
|
|
190
180
|
if (fetchService) {
|
|
@@ -199,10 +189,7 @@ roles: user.roles ?? [] } : undefined,
|
|
|
199
189
|
throw ApiError.notFound("Entity not found");
|
|
200
190
|
}
|
|
201
191
|
|
|
202
|
-
|
|
203
|
-
if (!entity) {
|
|
204
|
-
throw ApiError.notFound("Entity not found");
|
|
205
|
-
}
|
|
192
|
+
|
|
206
193
|
|
|
207
194
|
return c.json(entity);
|
|
208
195
|
}
|
|
@@ -214,10 +201,7 @@ roles: user.roles ?? [] } : undefined,
|
|
|
214
201
|
throw ApiError.notFound("Entity not found");
|
|
215
202
|
}
|
|
216
203
|
|
|
217
|
-
|
|
218
|
-
if (!entity) {
|
|
219
|
-
throw ApiError.notFound("Entity not found");
|
|
220
|
-
}
|
|
204
|
+
|
|
221
205
|
|
|
222
206
|
return c.json(entity);
|
|
223
207
|
});
|
|
@@ -228,13 +212,11 @@ roles: user.roles ?? [] } : undefined,
|
|
|
228
212
|
this.enforceApiKeyPermission(c, collection.slug);
|
|
229
213
|
const driver = this.getScopedDriver(c);
|
|
230
214
|
const path = collection.slug;
|
|
231
|
-
|
|
215
|
+
|
|
232
216
|
|
|
233
217
|
let body = await c.req.json().catch(() => ({}));
|
|
234
218
|
|
|
235
|
-
|
|
236
|
-
body = await this.dataHooks.beforeSave(path, body, undefined, hookCtx);
|
|
237
|
-
}
|
|
219
|
+
|
|
238
220
|
|
|
239
221
|
const isAuth = collection.auth;
|
|
240
222
|
const isAuthCollection = isAuth === true || (isAuth && typeof isAuth === "object" && isAuth.enabled === true);
|
|
@@ -263,11 +245,7 @@ values: entity.values as Record<string, unknown> },
|
|
|
263
245
|
|
|
264
246
|
const response = this.formatResponse(entity) as Record<string, unknown>;
|
|
265
247
|
|
|
266
|
-
|
|
267
|
-
Promise.resolve(this.dataHooks.afterSave(path, response, hookCtx)).catch(err => {
|
|
268
|
-
logger.error("[BackendHooks] data.afterSave error", { error: err instanceof Error ? err.message : err });
|
|
269
|
-
});
|
|
270
|
-
}
|
|
248
|
+
|
|
271
249
|
|
|
272
250
|
return c.json({
|
|
273
251
|
...response,
|
|
@@ -285,11 +263,7 @@ values: entity.values as Record<string, unknown> },
|
|
|
285
263
|
|
|
286
264
|
const response = this.formatResponse(entity);
|
|
287
265
|
|
|
288
|
-
|
|
289
|
-
Promise.resolve(this.dataHooks.afterSave(path, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
290
|
-
logger.error("[BackendHooks] data.afterSave error", { error: err instanceof Error ? err.message : err });
|
|
291
|
-
});
|
|
292
|
-
}
|
|
266
|
+
|
|
293
267
|
|
|
294
268
|
return c.json(response, 201);
|
|
295
269
|
} catch (error) {
|
|
@@ -315,10 +289,10 @@ values: entity.values as Record<string, unknown> },
|
|
|
315
289
|
this.enforceApiKeyPermission(c, collection.slug);
|
|
316
290
|
const id = c.req.param("id");
|
|
317
291
|
const driver = this.getScopedDriver(c);
|
|
318
|
-
|
|
292
|
+
|
|
319
293
|
|
|
320
294
|
const existingEntity = await driver.fetchEntity({
|
|
321
|
-
path: collection
|
|
295
|
+
path: getCollectionDataPath(collection),
|
|
322
296
|
entityId: String(id),
|
|
323
297
|
collection: resolvedCollection
|
|
324
298
|
});
|
|
@@ -329,12 +303,10 @@ values: entity.values as Record<string, unknown> },
|
|
|
329
303
|
|
|
330
304
|
let body = await c.req.json().catch(() => ({}));
|
|
331
305
|
|
|
332
|
-
|
|
333
|
-
body = await this.dataHooks.beforeSave(collection.slug, body, String(id), hookCtx);
|
|
334
|
-
}
|
|
306
|
+
|
|
335
307
|
|
|
336
308
|
const entity = await driver.saveEntity({
|
|
337
|
-
path: collection
|
|
309
|
+
path: getCollectionDataPath(collection),
|
|
338
310
|
entityId: String(id),
|
|
339
311
|
values: body,
|
|
340
312
|
collection: resolvedCollection,
|
|
@@ -343,11 +315,7 @@ values: entity.values as Record<string, unknown> },
|
|
|
343
315
|
|
|
344
316
|
const response = this.formatResponse(entity);
|
|
345
317
|
|
|
346
|
-
|
|
347
|
-
Promise.resolve(this.dataHooks.afterSave(collection.slug, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
348
|
-
logger.error("[BackendHooks] data.afterSave error", { error: err instanceof Error ? err.message : err });
|
|
349
|
-
});
|
|
350
|
-
}
|
|
318
|
+
|
|
351
319
|
|
|
352
320
|
return c.json(response);
|
|
353
321
|
} catch (error) {
|
|
@@ -371,10 +339,10 @@ values: entity.values as Record<string, unknown> },
|
|
|
371
339
|
this.enforceApiKeyPermission(c, collection.slug);
|
|
372
340
|
const id = c.req.param("id");
|
|
373
341
|
const driver = this.getScopedDriver(c);
|
|
374
|
-
|
|
342
|
+
|
|
375
343
|
|
|
376
344
|
const existingEntity = await driver.fetchEntity({
|
|
377
|
-
path: collection
|
|
345
|
+
path: getCollectionDataPath(collection),
|
|
378
346
|
entityId: String(id),
|
|
379
347
|
collection: resolvedCollection
|
|
380
348
|
});
|
|
@@ -383,20 +351,14 @@ values: entity.values as Record<string, unknown> },
|
|
|
383
351
|
throw ApiError.notFound("Entity not found");
|
|
384
352
|
}
|
|
385
353
|
|
|
386
|
-
|
|
387
|
-
await this.dataHooks.beforeDelete(collection.slug, String(id), hookCtx);
|
|
388
|
-
}
|
|
354
|
+
|
|
389
355
|
|
|
390
356
|
await driver.deleteEntity({
|
|
391
357
|
entity: existingEntity,
|
|
392
358
|
collection: resolvedCollection
|
|
393
359
|
});
|
|
394
360
|
|
|
395
|
-
|
|
396
|
-
Promise.resolve(this.dataHooks.afterDelete(collection.slug, String(id), hookCtx)).catch(err => {
|
|
397
|
-
logger.error("[BackendHooks] data.afterDelete error", { error: err instanceof Error ? err.message : err });
|
|
398
|
-
});
|
|
399
|
-
}
|
|
361
|
+
|
|
400
362
|
|
|
401
363
|
return new Response(null, { status: 204 });
|
|
402
364
|
});
|
|
@@ -464,7 +426,7 @@ entityId };
|
|
|
464
426
|
|
|
465
427
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
466
428
|
|
|
467
|
-
|
|
429
|
+
|
|
468
430
|
|
|
469
431
|
if (parsed.entityId === "count") {
|
|
470
432
|
// GET /parent/:parentId/child/count — count child entities
|
|
@@ -474,7 +436,7 @@ entityId };
|
|
|
474
436
|
|
|
475
437
|
const total = driver.countEntities ? await driver.countEntities({
|
|
476
438
|
path: parsed.collectionPath,
|
|
477
|
-
filter: queryOptions.where
|
|
439
|
+
filter: queryOptions.where,
|
|
478
440
|
searchString
|
|
479
441
|
}) : 0;
|
|
480
442
|
|
|
@@ -487,11 +449,7 @@ entityId };
|
|
|
487
449
|
});
|
|
488
450
|
if (!entity) throw ApiError.notFound("Entity not found");
|
|
489
451
|
|
|
490
|
-
|
|
491
|
-
const hookResult = await this.applyAfterRead(c.req.param("parent"), flatResult, hookCtx);
|
|
492
|
-
if (!hookResult) throw ApiError.notFound("Entity not found");
|
|
493
|
-
|
|
494
|
-
return c.json(hookResult);
|
|
452
|
+
return c.json(this.flattenEntity(entity));
|
|
495
453
|
} else {
|
|
496
454
|
// GET /parent/:parentId/child — list entities
|
|
497
455
|
const queryDict = c.req.queries();
|
|
@@ -499,19 +457,18 @@ entityId };
|
|
|
499
457
|
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : undefined;
|
|
500
458
|
const entities = await driver.fetchCollection({
|
|
501
459
|
path: parsed.collectionPath,
|
|
502
|
-
filter: queryOptions.where
|
|
460
|
+
filter: queryOptions.where,
|
|
503
461
|
limit: queryOptions.limit,
|
|
504
462
|
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
505
463
|
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
506
464
|
searchString
|
|
507
465
|
});
|
|
508
466
|
|
|
509
|
-
|
|
510
|
-
flatEntities = await this.applyAfterReadBatch(c.req.param("parent"), flatEntities, hookCtx);
|
|
467
|
+
const flatEntities = entities.map(e => this.flattenEntity(e));
|
|
511
468
|
|
|
512
469
|
const total = driver.countEntities ? await driver.countEntities({
|
|
513
470
|
path: parsed.collectionPath,
|
|
514
|
-
filter: queryOptions.where
|
|
471
|
+
filter: queryOptions.where,
|
|
515
472
|
searchString
|
|
516
473
|
}) : flatEntities.length;
|
|
517
474
|
|
|
@@ -536,14 +493,12 @@ entityId };
|
|
|
536
493
|
if (!parsed || parsed.entityId) return next();
|
|
537
494
|
|
|
538
495
|
const driver = this.getScopedDriver(c);
|
|
539
|
-
|
|
496
|
+
|
|
540
497
|
|
|
541
498
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
542
499
|
let body = await c.req.json().catch(() => ({}));
|
|
543
500
|
|
|
544
|
-
|
|
545
|
-
body = await this.dataHooks.beforeSave(parsed.collectionPath, body, undefined, hookCtx);
|
|
546
|
-
}
|
|
501
|
+
|
|
547
502
|
|
|
548
503
|
const entity = await driver.saveEntity({
|
|
549
504
|
path: parsed.collectionPath,
|
|
@@ -553,11 +508,7 @@ entityId };
|
|
|
553
508
|
|
|
554
509
|
const response = this.formatResponse(entity);
|
|
555
510
|
|
|
556
|
-
|
|
557
|
-
Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
558
|
-
logger.error("[BackendHooks] data.afterSave error", { error: err instanceof Error ? err.message : err });
|
|
559
|
-
});
|
|
560
|
-
}
|
|
511
|
+
|
|
561
512
|
|
|
562
513
|
return c.json(response, 201);
|
|
563
514
|
});
|
|
@@ -571,15 +522,13 @@ entityId };
|
|
|
571
522
|
if (!parsed || !parsed.entityId) return next();
|
|
572
523
|
|
|
573
524
|
const driver = this.getScopedDriver(c);
|
|
574
|
-
|
|
525
|
+
|
|
575
526
|
|
|
576
527
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
577
528
|
|
|
578
529
|
let body = await c.req.json().catch(() => ({}));
|
|
579
530
|
|
|
580
|
-
|
|
581
|
-
body = await this.dataHooks.beforeSave(parsed.collectionPath, body, parsed.entityId, hookCtx);
|
|
582
|
-
}
|
|
531
|
+
|
|
583
532
|
|
|
584
533
|
const entity = await driver.saveEntity({
|
|
585
534
|
path: parsed.collectionPath,
|
|
@@ -590,11 +539,7 @@ entityId };
|
|
|
590
539
|
|
|
591
540
|
const response = this.formatResponse(entity);
|
|
592
541
|
|
|
593
|
-
|
|
594
|
-
Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
595
|
-
logger.error("[BackendHooks] data.afterSave error", { error: err instanceof Error ? err.message : err });
|
|
596
|
-
});
|
|
597
|
-
}
|
|
542
|
+
|
|
598
543
|
|
|
599
544
|
return c.json(response);
|
|
600
545
|
});
|
|
@@ -608,7 +553,7 @@ entityId };
|
|
|
608
553
|
if (!parsed || !parsed.entityId) return next();
|
|
609
554
|
|
|
610
555
|
const driver = this.getScopedDriver(c);
|
|
611
|
-
|
|
556
|
+
|
|
612
557
|
|
|
613
558
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
614
559
|
|
|
@@ -619,17 +564,11 @@ entityId };
|
|
|
619
564
|
|
|
620
565
|
if (!existingEntity) throw ApiError.notFound("Entity not found");
|
|
621
566
|
|
|
622
|
-
|
|
623
|
-
await this.dataHooks.beforeDelete(parsed.collectionPath, parsed.entityId, hookCtx);
|
|
624
|
-
}
|
|
567
|
+
|
|
625
568
|
|
|
626
569
|
await driver.deleteEntity({ entity: existingEntity });
|
|
627
570
|
|
|
628
|
-
|
|
629
|
-
Promise.resolve(this.dataHooks.afterDelete(parsed.collectionPath, parsed.entityId, hookCtx)).catch(err => {
|
|
630
|
-
logger.error("[BackendHooks] data.afterDelete error", { error: err instanceof Error ? err.message : err });
|
|
631
|
-
});
|
|
632
|
-
}
|
|
571
|
+
|
|
633
572
|
|
|
634
573
|
return new Response(null, { status: 204 });
|
|
635
574
|
});
|
|
@@ -686,9 +625,9 @@ entityId };
|
|
|
686
625
|
*/
|
|
687
626
|
private async fetchRawCollection(driver: DataDriver, collection: EntityCollection, queryOptions: QueryOptions, searchString?: string) {
|
|
688
627
|
const entities = await driver.fetchCollection({
|
|
689
|
-
path: collection
|
|
628
|
+
path: getCollectionDataPath(collection),
|
|
690
629
|
collection,
|
|
691
|
-
filter: queryOptions.where
|
|
630
|
+
filter: queryOptions.where,
|
|
692
631
|
limit: queryOptions.limit,
|
|
693
632
|
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
694
633
|
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
@@ -705,9 +644,9 @@ entityId };
|
|
|
705
644
|
*/
|
|
706
645
|
private async countRawEntities(driver: DataDriver, collection: EntityCollection, queryOptions: QueryOptions, searchString?: string): Promise<number> {
|
|
707
646
|
return driver.countEntities ? await driver.countEntities({
|
|
708
|
-
path: collection
|
|
647
|
+
path: getCollectionDataPath(collection),
|
|
709
648
|
collection,
|
|
710
|
-
filter: queryOptions.where
|
|
649
|
+
filter: queryOptions.where,
|
|
711
650
|
searchString
|
|
712
651
|
}) : 0;
|
|
713
652
|
}
|
|
@@ -717,7 +656,7 @@ entityId };
|
|
|
717
656
|
*/
|
|
718
657
|
private async fetchRawEntity(driver: DataDriver, collection: EntityCollection, entityId: string) {
|
|
719
658
|
const entity = await driver.fetchEntity({
|
|
720
|
-
path: collection
|
|
659
|
+
path: getCollectionDataPath(collection),
|
|
721
660
|
entityId,
|
|
722
661
|
collection
|
|
723
662
|
});
|
|
@@ -725,23 +664,5 @@ entityId };
|
|
|
725
664
|
return entity ? this.flattenEntity(entity) : null;
|
|
726
665
|
}
|
|
727
666
|
|
|
728
|
-
/**
|
|
729
|
-
* Apply data.afterRead hook to a single entity.
|
|
730
|
-
* Returns the transformed entity, or null to filter it out.
|
|
731
|
-
*/
|
|
732
|
-
private async applyAfterRead(slug: string, entity: Record<string, unknown>, ctx: BackendHookContext): Promise<Record<string, unknown> | null> {
|
|
733
|
-
if (!this.dataHooks?.afterRead) return entity;
|
|
734
|
-
return this.dataHooks.afterRead(slug, entity, ctx);
|
|
735
|
-
}
|
|
736
667
|
|
|
737
|
-
/**
|
|
738
|
-
* Apply data.afterRead hook to an array of entities, filtering out nulls.
|
|
739
|
-
*/
|
|
740
|
-
private async applyAfterReadBatch(slug: string, entities: Record<string, unknown>[], ctx: BackendHookContext): Promise<Record<string, unknown>[]> {
|
|
741
|
-
if (!this.dataHooks?.afterRead) return entities;
|
|
742
|
-
const results = await Promise.all(
|
|
743
|
-
entities.map(e => this.applyAfterRead(slug, e, ctx))
|
|
744
|
-
);
|
|
745
|
-
return results.filter((e): e is Record<string, unknown> => e !== null);
|
|
746
|
-
}
|
|
747
668
|
}
|
|
@@ -1,25 +1,8 @@
|
|
|
1
|
-
import type { VectorSearchParams, LogicalCondition, FilterCondition } from "@rebasepro/types";
|
|
1
|
+
import type { VectorSearchParams, LogicalCondition, FilterCondition, WhereFilterOp } from "@rebasepro/types";
|
|
2
|
+
import { toCanonicalOp } from "@rebasepro/types";
|
|
3
|
+
export const mapOperator = (op: string) => toCanonicalOp(op) ?? null;
|
|
2
4
|
import { QueryOptions } from "../types";
|
|
3
5
|
|
|
4
|
-
/**
|
|
5
|
-
* Map PostgREST-style operators to Rebase WhereFilterOp
|
|
6
|
-
*/
|
|
7
|
-
export function mapOperator(op: string): string | null {
|
|
8
|
-
switch (op) {
|
|
9
|
-
case "eq": return "==";
|
|
10
|
-
case "neq": return "!=";
|
|
11
|
-
case "gt": return ">";
|
|
12
|
-
case "gte": return ">=";
|
|
13
|
-
case "lt": return "<";
|
|
14
|
-
case "lte": return "<=";
|
|
15
|
-
case "in": return "in";
|
|
16
|
-
case "nin": return "not-in";
|
|
17
|
-
case "cs": return "array-contains";
|
|
18
|
-
case "csa": return "array-contains-any";
|
|
19
|
-
default: return null;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
6
|
function getLastValue(val: unknown): unknown {
|
|
24
7
|
if (Array.isArray(val)) {
|
|
25
8
|
return val[val.length - 1];
|
|
@@ -60,7 +43,7 @@ value: true };
|
|
|
60
43
|
valStr = rest;
|
|
61
44
|
}
|
|
62
45
|
|
|
63
|
-
const rebaseOp = (
|
|
46
|
+
const rebaseOp = (toCanonicalOp(op) ?? "==") as FilterCondition["operator"];
|
|
64
47
|
let parsedVal: unknown = valStr;
|
|
65
48
|
if (valStr === "true") parsedVal = true;
|
|
66
49
|
else if (valStr === "false") parsedVal = false;
|
|
@@ -158,7 +141,7 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
|
|
|
158
141
|
if (reservedQueryKeys.includes(key)) continue;
|
|
159
142
|
|
|
160
143
|
const rawValues = Array.isArray(rawValue) ? rawValue : [rawValue];
|
|
161
|
-
const conditions: [
|
|
144
|
+
const conditions: [WhereFilterOp, unknown][] = [];
|
|
162
145
|
|
|
163
146
|
for (const value of rawValues) {
|
|
164
147
|
if (typeof value === "string") {
|
|
@@ -166,7 +149,7 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
|
|
|
166
149
|
if (parts.length >= 2) {
|
|
167
150
|
const op = parts[0];
|
|
168
151
|
const val = parts.slice(1).join(".");
|
|
169
|
-
const rebaseOp =
|
|
152
|
+
const rebaseOp = toCanonicalOp(op);
|
|
170
153
|
|
|
171
154
|
if (rebaseOp) {
|
|
172
155
|
let parsedVal: string | number | boolean | null | (string | number | boolean | null)[] = val;
|
package/src/api/server.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { serve } from "@hono/node-server";
|
|
|
6
6
|
import { GraphQLSchemaGenerator } from "./graphql/graphql-schema-generator";
|
|
7
7
|
import { RestApiGenerator } from "./rest/api-generator";
|
|
8
8
|
import { DataDriver, EntityCollection, Relation } from "@rebasepro/types";
|
|
9
|
+
import { createDataSourceRegistry, resolveDataSource } from "@rebasepro/common";
|
|
9
10
|
import { ApiConfig, HonoEnv } from "./types";
|
|
10
11
|
import { loadCollectionsFromDirectory } from "../collections/loader";
|
|
11
12
|
import { createSchemaEditorRoutes } from "./schema-editor-routes";
|
|
@@ -142,9 +143,16 @@ export class RebaseApiServer {
|
|
|
142
143
|
return c.json({ data: collectionsMetadata });
|
|
143
144
|
});
|
|
144
145
|
|
|
146
|
+
// Server data routes are only generated for server-transport
|
|
147
|
+
// collections; direct/custom sources are client-only.
|
|
148
|
+
const dataSourceRegistry = createDataSourceRegistry(this.config.dataSources);
|
|
149
|
+
const serverCollections = (this.config.collections || []).filter(
|
|
150
|
+
(collection) => resolveDataSource(collection, dataSourceRegistry).transport === "server"
|
|
151
|
+
);
|
|
152
|
+
|
|
145
153
|
// GraphQL endpoint
|
|
146
154
|
if (this.config.enableGraphQL) {
|
|
147
|
-
const schemaGenerator = new GraphQLSchemaGenerator(
|
|
155
|
+
const schemaGenerator = new GraphQLSchemaGenerator(serverCollections, this.driver);
|
|
148
156
|
const schema = schemaGenerator.generateSchema();
|
|
149
157
|
|
|
150
158
|
// Context is automatically passed to resolvers via contextValue containing Hono's 'c'
|
|
@@ -182,7 +190,7 @@ export class RebaseApiServer {
|
|
|
182
190
|
}
|
|
183
191
|
|
|
184
192
|
if (this.config.enableREST) {
|
|
185
|
-
const restGenerator = new RestApiGenerator(
|
|
193
|
+
const restGenerator = new RestApiGenerator(serverCollections, this.driver);
|
|
186
194
|
const restRoutes = restGenerator.generateRoutes();
|
|
187
195
|
this.router.route(basePath, restRoutes);
|
|
188
196
|
}
|
package/src/api/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityCollection, VectorSearchParams, LogicalCondition } from "@rebasepro/types";
|
|
1
|
+
import { EntityCollection, VectorSearchParams, LogicalCondition, FilterValues } from "@rebasepro/types";
|
|
2
2
|
import { AuthResult } from "../auth/middleware";
|
|
3
3
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
4
4
|
import { DataDriver } from "@rebasepro/types";
|
|
@@ -28,6 +28,12 @@ export type HonoEnv = {
|
|
|
28
28
|
export interface ApiConfig {
|
|
29
29
|
collections?: EntityCollection[];
|
|
30
30
|
collectionsDir?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Declared data sources. When provided, collections on a `direct`/`custom`
|
|
33
|
+
* transport are treated as client-only and no server routes are generated
|
|
34
|
+
* for them.
|
|
35
|
+
*/
|
|
36
|
+
dataSources?: import("@rebasepro/types").DataSourceDefinition[];
|
|
31
37
|
basePath?: string;
|
|
32
38
|
enableGraphQL?: boolean;
|
|
33
39
|
enableREST?: boolean;
|
|
@@ -78,7 +84,7 @@ export interface ApiResponse<T = unknown> {
|
|
|
78
84
|
export interface QueryOptions {
|
|
79
85
|
limit?: number;
|
|
80
86
|
offset?: number;
|
|
81
|
-
where?:
|
|
87
|
+
where?: FilterValues<string>;
|
|
82
88
|
logical?: LogicalCondition;
|
|
83
89
|
orderBy?: Array<{ field: string; direction: "asc" | "desc" }>;
|
|
84
90
|
include?: string[];
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* token verification strategy is pluggable.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import type { MiddlewareHandler } from "hono";
|
|
18
|
+
import type { MiddlewareHandler, Context } from "hono";
|
|
19
19
|
import type { DataDriver, AuthAdapter } from "@rebasepro/types";
|
|
20
20
|
import type { HonoEnv } from "../api/types";
|
|
21
21
|
import type { ApiKeyStore } from "./api-keys/api-key-store";
|
|
@@ -28,6 +28,12 @@ export interface AdapterAuthMiddlewareOptions {
|
|
|
28
28
|
adapter: AuthAdapter;
|
|
29
29
|
/** The DataDriver to scope via withAuth() for RLS. */
|
|
30
30
|
driver: DataDriver;
|
|
31
|
+
/**
|
|
32
|
+
* Optional per-request driver resolver for multi-data-source backends.
|
|
33
|
+
* Returns the unscoped delegate to use for this request (e.g. Postgres vs
|
|
34
|
+
* Mongo). When omitted, `driver` is used for every request.
|
|
35
|
+
*/
|
|
36
|
+
resolveDriver?: (c: Context<HonoEnv>) => DataDriver;
|
|
31
37
|
/**
|
|
32
38
|
* If true, return 401 when no valid user is resolved.
|
|
33
39
|
* Defaults to `true` (secure by default).
|
|
@@ -41,9 +47,11 @@ export interface AdapterAuthMiddlewareOptions {
|
|
|
41
47
|
* Create a Hono middleware that uses an `AuthAdapter` for request verification.
|
|
42
48
|
*/
|
|
43
49
|
export function createAdapterAuthMiddleware(options: AdapterAuthMiddlewareOptions): MiddlewareHandler<HonoEnv> {
|
|
44
|
-
const { adapter, driver, requireAuth: enforceAuth = true, apiKeyStore } = options;
|
|
50
|
+
const { adapter, driver: baseDriver, resolveDriver, requireAuth: enforceAuth = true, apiKeyStore } = options;
|
|
45
51
|
|
|
46
52
|
return async (c, next) => {
|
|
53
|
+
// Pick the per-request delegate (multi-data-source) before scoping.
|
|
54
|
+
const driver = resolveDriver ? resolveDriver(c) : baseDriver;
|
|
47
55
|
// ── API Key check (Rebase-level, independent of auth adapter) ────
|
|
48
56
|
if (apiKeyStore) {
|
|
49
57
|
const authHeader = c.req.header("authorization") || "";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin endpoint for listing all roles.
|
|
3
|
+
*
|
|
4
|
+
* Mounts: GET /roles
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import { errorHandler } from "../api/errors";
|
|
9
|
+
import type { AuthRepository } from "./interfaces";
|
|
10
|
+
import { createRequireAuth, requireAdmin } from "./middleware";
|
|
11
|
+
import type { HonoEnv } from "../api/types";
|
|
12
|
+
|
|
13
|
+
export interface AdminRolesRouteConfig {
|
|
14
|
+
authRepo: AuthRepository;
|
|
15
|
+
serviceKey?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a standalone admin route for listing roles.
|
|
20
|
+
*
|
|
21
|
+
* Mounts: GET /roles
|
|
22
|
+
*/
|
|
23
|
+
export function createAdminRolesRoute(config: AdminRolesRouteConfig): Hono<HonoEnv> {
|
|
24
|
+
const router = new Hono<HonoEnv>();
|
|
25
|
+
const authRepo = config.authRepo;
|
|
26
|
+
|
|
27
|
+
router.onError(errorHandler);
|
|
28
|
+
router.use("/*", createRequireAuth({ serviceKey: config.serviceKey }));
|
|
29
|
+
|
|
30
|
+
router.get("/roles", requireAdmin, async (c) => {
|
|
31
|
+
const roles = await authRepo.listRoles();
|
|
32
|
+
return c.json({ roles });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return router;
|
|
36
|
+
}
|