@rebasepro/server-core 0.4.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/util/permissions.d.ts +14 -6
- package/dist/index.es.js +75 -33
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +75 -33
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/errors.d.ts +15 -0
- package/dist/server-core/src/auth/jwt.d.ts +10 -0
- package/dist/types/src/types/backend.d.ts +36 -1
- package/dist/types/src/types/collections.d.ts +21 -1
- package/dist/types/src/types/properties.d.ts +0 -8
- 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 +71 -15
- package/src/api/server.ts +1 -1
- package/src/auth/builtin-auth-adapter.ts +4 -15
- package/src/auth/jwt.ts +10 -0
- package/src/auth/routes.ts +5 -9
- package/src/init.ts +12 -20
- package/test.ts +0 -6
package/README.md
CHANGED
|
@@ -1,40 +1,77 @@
|
|
|
1
|
-
# @rebasepro/
|
|
1
|
+
# @rebasepro/server-core
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
This package provides a complete backend solution for Rebase applications using PostgreSQL as the database and Drizzle ORM for type-safe database operations.
|
|
3
|
+
Database-agnostic backend core for Rebase.
|
|
6
4
|
|
|
7
5
|
## Installation
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
|
-
|
|
8
|
+
pnpm add @rebasepro/server-core
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
##
|
|
11
|
+
## What This Package Does
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
import { createBackend } from "@rebasepro/backend";
|
|
13
|
+
This is the central orchestrator for any Rebase backend. It provides the framework-level plumbing — HTTP routing (Hono), authentication middleware, storage, email, cron jobs, custom functions, and the REST/GraphQL API generator — without being coupled to any specific database. Database implementations are plugged in via driver packages like `@rebasepro/server-postgresql` or `@rebasepro/server-mongodb`.
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
## Key Exports
|
|
16
|
+
|
|
17
|
+
| Export | Description |
|
|
18
|
+
|--------|-------------|
|
|
19
|
+
| `initializeRebaseBackend(config)` | Main entry point. Wires up drivers, auth, storage, API routes, cron, and custom functions. Returns a `RebaseBackendInstance`. |
|
|
20
|
+
| `rebase` | Server-side singleton (`RebaseClient`). Available after init. Admin-level access to data, auth, email, and storage. |
|
|
21
|
+
| `loadEnv()` | Validates `process.env` against the Rebase env schema (Zod). Auto-generates dev secrets. Supports `extend` for custom vars. |
|
|
22
|
+
| `serveSPA(app, config)` | Mounts SPA static-file serving + index.html fallback on a Hono app. |
|
|
23
|
+
| `RebaseBackendConfig` | Config type for `initializeRebaseBackend`. |
|
|
24
|
+
| `RebaseAuthConfig` | Auth config type (JWT, OAuth providers, hooks, service key). |
|
|
25
|
+
| `RebaseBackendInstance` | Return type — includes `driver`, `healthCheck()`, `shutdown()`, `cronScheduler`, `storageController`, etc. |
|
|
26
|
+
| `RebaseEnv` | Zod-inferred type of validated environment variables. |
|
|
27
|
+
| `z` | Re-exported Zod instance for extending `loadEnv`. |
|
|
28
|
+
| `_setRebaseMock` / `_resetRebaseMock` | Test helpers to mock the `rebase` singleton (NODE_ENV=test only). |
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
Also re-exports all abstract interfaces (`DatabaseAdapter`, `AuthAdapter`, `DataDriver`, etc.), API types (`HonoEnv`, `ApiConfig`), auth module, email module, storage module, history module, cron module, custom functions, logging utilities, and driver registry.
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
- Drizzle ORM integration
|
|
29
|
-
- Type-safe database operations
|
|
30
|
-
- Full Rebase compatibility
|
|
31
|
-
- Migration support
|
|
32
|
-
- Connection pooling
|
|
32
|
+
## Quick Start
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
```typescript
|
|
35
|
+
import { serve } from "@hono/node-server";
|
|
36
|
+
import { Hono } from "hono";
|
|
37
|
+
import { initializeRebaseBackend, loadEnv, serveSPA } from "@rebasepro/server-core";
|
|
38
|
+
import { createPostgresAdapter } from "@rebasepro/server-postgresql";
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
// 1. Load and validate environment
|
|
41
|
+
const env = loadEnv();
|
|
42
|
+
|
|
43
|
+
// 2. Create Hono app + HTTP server
|
|
44
|
+
const app = new Hono();
|
|
45
|
+
const server = serve({ fetch: app.fetch, port: env.PORT });
|
|
46
|
+
|
|
47
|
+
// 3. Initialize Rebase
|
|
48
|
+
const backend = await initializeRebaseBackend({
|
|
49
|
+
app,
|
|
50
|
+
server,
|
|
51
|
+
database: createPostgresAdapter({ connection: db, schema }),
|
|
52
|
+
collections: myCollections,
|
|
53
|
+
auth: {
|
|
54
|
+
collection: defaultUsersCollection,
|
|
55
|
+
jwtSecret: env.JWT_SECRET,
|
|
56
|
+
allowRegistration: env.ALLOW_REGISTRATION,
|
|
57
|
+
serviceKey: env.REBASE_SERVICE_KEY,
|
|
58
|
+
google: { clientId: env.GOOGLE_CLIENT_ID },
|
|
59
|
+
},
|
|
60
|
+
storage: { type: "s3", bucket: env.S3_BUCKET },
|
|
61
|
+
functionsDir: "./functions",
|
|
62
|
+
cronsDir: "./crons",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 4. Optionally serve a frontend SPA
|
|
66
|
+
serveSPA(app, { frontendPath: "./frontend/dist" });
|
|
67
|
+
```
|
|
37
68
|
|
|
38
|
-
##
|
|
69
|
+
## Related Packages
|
|
39
70
|
|
|
40
|
-
|
|
71
|
+
| Package | Role |
|
|
72
|
+
|---------|------|
|
|
73
|
+
| `@rebasepro/server-postgresql` | PostgreSQL database driver (Drizzle ORM) |
|
|
74
|
+
| `@rebasepro/server-mongodb` | MongoDB database driver |
|
|
75
|
+
| `@rebasepro/types` | Shared type definitions (`DataDriver`, `EntityCollection`, etc.) |
|
|
76
|
+
| `@rebasepro/client` | Client SDK used internally by the `rebase` singleton |
|
|
77
|
+
| `@rebasepro/common` | Shared utilities and default collections |
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { Entity, EntityCollection, User } from "@rebasepro/types";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal auth context for permission checking.
|
|
4
|
+
* Only requires the user object — avoids forcing callers to construct
|
|
5
|
+
* a full AuthController just to check permissions.
|
|
6
|
+
*/
|
|
7
|
+
export interface AuthContext<USER extends User = User> {
|
|
8
|
+
user: USER | null;
|
|
9
|
+
}
|
|
10
|
+
export declare function checkOperation<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>, entity: Entity<M> | null, targetOperation: "select" | "insert" | "update" | "delete"): boolean;
|
|
11
|
+
export declare function canReadCollection<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>): boolean;
|
|
12
|
+
export declare function canEditEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>, path: string, entity: Entity<M> | null): boolean;
|
|
13
|
+
export declare function canCreateEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>, path: string, entity: Entity<M> | null): boolean;
|
|
14
|
+
export declare function canDeleteEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>, path: string, entity: Entity<M> | null): boolean;
|
package/dist/index.es.js
CHANGED
|
@@ -2790,6 +2790,9 @@ class ApiError extends Error {
|
|
|
2790
2790
|
return new ApiError(503, code2, message);
|
|
2791
2791
|
}
|
|
2792
2792
|
}
|
|
2793
|
+
function isRebaseApiError(error2) {
|
|
2794
|
+
return error2 instanceof Error;
|
|
2795
|
+
}
|
|
2793
2796
|
const errorHandler = (err, c) => {
|
|
2794
2797
|
const error2 = err;
|
|
2795
2798
|
if (error2 instanceof ApiError || error2.name === "ApiError") {
|
|
@@ -3330,9 +3333,10 @@ class RestApiGenerator {
|
|
|
3330
3333
|
}
|
|
3331
3334
|
return c.json(response, 201);
|
|
3332
3335
|
} catch (error2) {
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
+
if (isRebaseApiError(error2) && !error2.code) {
|
|
3337
|
+
error2.code = "BAD_REQUEST";
|
|
3338
|
+
}
|
|
3339
|
+
throw error2;
|
|
3336
3340
|
}
|
|
3337
3341
|
});
|
|
3338
3342
|
this.router.put(`${basePath}/:id`, async (c) => {
|
|
@@ -3368,9 +3372,10 @@ class RestApiGenerator {
|
|
|
3368
3372
|
}
|
|
3369
3373
|
return c.json(response);
|
|
3370
3374
|
} catch (error2) {
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3375
|
+
if (isRebaseApiError(error2) && !error2.code) {
|
|
3376
|
+
error2.code = "BAD_REQUEST";
|
|
3377
|
+
}
|
|
3378
|
+
throw error2;
|
|
3374
3379
|
}
|
|
3375
3380
|
});
|
|
3376
3381
|
this.router.delete(`${basePath}/:id`, async (c) => {
|
|
@@ -3447,6 +3452,7 @@ class RestApiGenerator {
|
|
|
3447
3452
|
if (!parsed) return next();
|
|
3448
3453
|
const driver = c.get("driver") || this.driver;
|
|
3449
3454
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
3455
|
+
const hookCtx = this.buildHookContext(c, "GET");
|
|
3450
3456
|
if (parsed.entityId === "count") {
|
|
3451
3457
|
const queryDict = c.req.queries();
|
|
3452
3458
|
const queryOptions = parseQueryOptions(queryDict);
|
|
@@ -3465,7 +3471,10 @@ class RestApiGenerator {
|
|
|
3465
3471
|
entityId: parsed.entityId
|
|
3466
3472
|
});
|
|
3467
3473
|
if (!entity) throw ApiError.notFound("Entity not found");
|
|
3468
|
-
|
|
3474
|
+
const flatResult = this.flattenEntity(entity);
|
|
3475
|
+
const hookResult = await this.applyAfterRead(c.req.param("parent"), flatResult, hookCtx);
|
|
3476
|
+
if (!hookResult) throw ApiError.notFound("Entity not found");
|
|
3477
|
+
return c.json(hookResult);
|
|
3469
3478
|
} else {
|
|
3470
3479
|
const queryDict = c.req.queries();
|
|
3471
3480
|
const queryOptions = parseQueryOptions(queryDict);
|
|
@@ -3478,13 +3487,20 @@ class RestApiGenerator {
|
|
|
3478
3487
|
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
3479
3488
|
searchString
|
|
3480
3489
|
});
|
|
3490
|
+
let flatEntities = entities.map((e) => this.flattenEntity(e));
|
|
3491
|
+
flatEntities = await this.applyAfterReadBatch(c.req.param("parent"), flatEntities, hookCtx);
|
|
3492
|
+
const total = driver.countEntities ? await driver.countEntities({
|
|
3493
|
+
path: parsed.collectionPath,
|
|
3494
|
+
filter: queryOptions.where,
|
|
3495
|
+
searchString
|
|
3496
|
+
}) : flatEntities.length;
|
|
3481
3497
|
return c.json({
|
|
3482
|
-
data:
|
|
3498
|
+
data: flatEntities,
|
|
3483
3499
|
meta: {
|
|
3484
|
-
total
|
|
3500
|
+
total,
|
|
3485
3501
|
limit: queryOptions.limit,
|
|
3486
3502
|
offset: queryOptions.offset,
|
|
3487
|
-
hasMore:
|
|
3503
|
+
hasMore: (queryOptions.offset || 0) + flatEntities.length < total
|
|
3488
3504
|
}
|
|
3489
3505
|
});
|
|
3490
3506
|
}
|
|
@@ -3496,14 +3512,24 @@ class RestApiGenerator {
|
|
|
3496
3512
|
const parsed = parseSubPath(rawPath);
|
|
3497
3513
|
if (!parsed || parsed.entityId) return next();
|
|
3498
3514
|
const driver = c.get("driver") || this.driver;
|
|
3515
|
+
const hookCtx = this.buildHookContext(c, "POST");
|
|
3499
3516
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
3500
|
-
|
|
3517
|
+
let body = await c.req.json().catch(() => ({}));
|
|
3518
|
+
if (this.dataHooks?.beforeSave) {
|
|
3519
|
+
body = await this.dataHooks.beforeSave(parsed.collectionPath, body, void 0, hookCtx);
|
|
3520
|
+
}
|
|
3501
3521
|
const entity = await driver.saveEntity({
|
|
3502
3522
|
path: parsed.collectionPath,
|
|
3503
3523
|
values: body,
|
|
3504
3524
|
status: "new"
|
|
3505
3525
|
});
|
|
3506
|
-
|
|
3526
|
+
const response = this.formatResponse(entity);
|
|
3527
|
+
if (this.dataHooks?.afterSave) {
|
|
3528
|
+
Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response, hookCtx)).catch((err) => {
|
|
3529
|
+
console.error("[BackendHooks] data.afterSave error:", err instanceof Error ? err.message : err);
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
3532
|
+
return c.json(response, 201);
|
|
3507
3533
|
});
|
|
3508
3534
|
this.router.put("/:parent/:parentId/:rest{.+}", async (c, next) => {
|
|
3509
3535
|
const rest = c.req.param("rest");
|
|
@@ -3512,15 +3538,25 @@ class RestApiGenerator {
|
|
|
3512
3538
|
const parsed = parseSubPath(rawPath);
|
|
3513
3539
|
if (!parsed || !parsed.entityId) return next();
|
|
3514
3540
|
const driver = c.get("driver") || this.driver;
|
|
3541
|
+
const hookCtx = this.buildHookContext(c, "PUT");
|
|
3515
3542
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
3516
|
-
|
|
3543
|
+
let body = await c.req.json().catch(() => ({}));
|
|
3544
|
+
if (this.dataHooks?.beforeSave) {
|
|
3545
|
+
body = await this.dataHooks.beforeSave(parsed.collectionPath, body, parsed.entityId, hookCtx);
|
|
3546
|
+
}
|
|
3517
3547
|
const entity = await driver.saveEntity({
|
|
3518
3548
|
path: parsed.collectionPath,
|
|
3519
3549
|
entityId: parsed.entityId,
|
|
3520
3550
|
values: body,
|
|
3521
3551
|
status: "existing"
|
|
3522
3552
|
});
|
|
3523
|
-
|
|
3553
|
+
const response = this.formatResponse(entity);
|
|
3554
|
+
if (this.dataHooks?.afterSave) {
|
|
3555
|
+
Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response, hookCtx)).catch((err) => {
|
|
3556
|
+
console.error("[BackendHooks] data.afterSave error:", err instanceof Error ? err.message : err);
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3559
|
+
return c.json(response);
|
|
3524
3560
|
});
|
|
3525
3561
|
this.router.delete("/:parent/:parentId/:rest{.+}", async (c, next) => {
|
|
3526
3562
|
const rest = c.req.param("rest");
|
|
@@ -3529,15 +3565,24 @@ class RestApiGenerator {
|
|
|
3529
3565
|
const parsed = parseSubPath(rawPath);
|
|
3530
3566
|
if (!parsed || !parsed.entityId) return next();
|
|
3531
3567
|
const driver = c.get("driver") || this.driver;
|
|
3568
|
+
const hookCtx = this.buildHookContext(c, "DELETE");
|
|
3532
3569
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
3533
3570
|
const existingEntity = await driver.fetchEntity({
|
|
3534
3571
|
path: parsed.collectionPath,
|
|
3535
3572
|
entityId: parsed.entityId
|
|
3536
3573
|
});
|
|
3537
3574
|
if (!existingEntity) throw ApiError.notFound("Entity not found");
|
|
3575
|
+
if (this.dataHooks?.beforeDelete) {
|
|
3576
|
+
await this.dataHooks.beforeDelete(parsed.collectionPath, parsed.entityId, hookCtx);
|
|
3577
|
+
}
|
|
3538
3578
|
await driver.deleteEntity({
|
|
3539
3579
|
entity: existingEntity
|
|
3540
3580
|
});
|
|
3581
|
+
if (this.dataHooks?.afterDelete) {
|
|
3582
|
+
Promise.resolve(this.dataHooks.afterDelete(parsed.collectionPath, parsed.entityId, hookCtx)).catch((err) => {
|
|
3583
|
+
console.error("[BackendHooks] data.afterDelete error:", err instanceof Error ? err.message : err);
|
|
3584
|
+
});
|
|
3585
|
+
}
|
|
3541
3586
|
return new Response(null, {
|
|
3542
3587
|
status: 204
|
|
3543
3588
|
});
|
|
@@ -13020,14 +13065,11 @@ function createAuthRoutes(config) {
|
|
|
13020
13065
|
if (!factor || factor.userId !== userCtx.userId) {
|
|
13021
13066
|
throw ApiError.notFound("MFA factor not found");
|
|
13022
13067
|
}
|
|
13023
|
-
const
|
|
13024
|
-
let isValid2 =
|
|
13025
|
-
if (
|
|
13068
|
+
const secretBuffer = base32Decode(factor.secretEncrypted);
|
|
13069
|
+
let isValid2 = verifyTotp(secretBuffer, code2);
|
|
13070
|
+
if (!isValid2) {
|
|
13026
13071
|
const codeHash = hashRecoveryCode(code2);
|
|
13027
13072
|
isValid2 = await authRepo.useRecoveryCode(userCtx.userId, codeHash);
|
|
13028
|
-
} else {
|
|
13029
|
-
const secretBuffer = base32Decode(factor.secretEncrypted);
|
|
13030
|
-
isValid2 = verifyTotp(secretBuffer, code2);
|
|
13031
13073
|
}
|
|
13032
13074
|
if (!isValid2) {
|
|
13033
13075
|
throw ApiError.unauthorized("Invalid verification code", "INVALID_CODE");
|
|
@@ -13543,7 +13585,6 @@ function createBuiltinAuthAdapter(config) {
|
|
|
13543
13585
|
if (!payload) {
|
|
13544
13586
|
return null;
|
|
13545
13587
|
}
|
|
13546
|
-
const extendedPayload = payload;
|
|
13547
13588
|
let roles = payload.roles || [];
|
|
13548
13589
|
try {
|
|
13549
13590
|
roles = await authRepository.getUserRoleIds(payload.userId);
|
|
@@ -13552,8 +13593,8 @@ function createBuiltinAuthAdapter(config) {
|
|
|
13552
13593
|
const isAdmin = roles.some((r) => r === "admin" || r === "schema-admin");
|
|
13553
13594
|
return {
|
|
13554
13595
|
uid: payload.userId,
|
|
13555
|
-
email:
|
|
13556
|
-
displayName:
|
|
13596
|
+
email: payload.email ?? "",
|
|
13597
|
+
displayName: payload.displayName ?? null,
|
|
13557
13598
|
roles,
|
|
13558
13599
|
isAdmin,
|
|
13559
13600
|
rawToken: token
|
|
@@ -13573,7 +13614,6 @@ function createBuiltinAuthAdapter(config) {
|
|
|
13573
13614
|
if (!payload) {
|
|
13574
13615
|
return null;
|
|
13575
13616
|
}
|
|
13576
|
-
const extendedPayload = payload;
|
|
13577
13617
|
let roles = payload.roles || [];
|
|
13578
13618
|
try {
|
|
13579
13619
|
roles = await authRepository.getUserRoleIds(payload.userId);
|
|
@@ -13582,8 +13622,8 @@ function createBuiltinAuthAdapter(config) {
|
|
|
13582
13622
|
const isAdmin = roles.some((r) => r === "admin" || r === "schema-admin");
|
|
13583
13623
|
return {
|
|
13584
13624
|
uid: payload.userId,
|
|
13585
|
-
email:
|
|
13586
|
-
displayName:
|
|
13625
|
+
email: payload.email ?? "",
|
|
13626
|
+
displayName: payload.displayName ?? null,
|
|
13587
13627
|
roles,
|
|
13588
13628
|
isAdmin,
|
|
13589
13629
|
rawToken: token
|
|
@@ -38575,7 +38615,9 @@ async function _initializeRebaseBackend(config) {
|
|
|
38575
38615
|
}
|
|
38576
38616
|
if (bootstrapper.initializeRealtime) {
|
|
38577
38617
|
const realtime = await bootstrapper.initializeRealtime({}, driverResult);
|
|
38578
|
-
|
|
38618
|
+
if (realtime) {
|
|
38619
|
+
realtimeServices[b.id || bootstrapper.type] = realtime;
|
|
38620
|
+
}
|
|
38579
38621
|
}
|
|
38580
38622
|
}
|
|
38581
38623
|
const driverRegistry = DefaultDriverRegistry.create(delegates);
|
|
@@ -39054,7 +39096,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
39054
39096
|
});
|
|
39055
39097
|
}
|
|
39056
39098
|
}
|
|
39057
|
-
if (defaultBootstrapper.initializeWebsockets) {
|
|
39099
|
+
if (defaultBootstrapper.initializeWebsockets && defaultRealtimeService) {
|
|
39058
39100
|
await defaultBootstrapper.initializeWebsockets(config.server, defaultRealtimeService, defaultDriver, config.auth, authAdapter);
|
|
39059
39101
|
}
|
|
39060
39102
|
logger.info("Rebase Backend Initialized");
|
|
@@ -39100,12 +39142,11 @@ async function _initializeRebaseBackend(config) {
|
|
|
39100
39142
|
}
|
|
39101
39143
|
for (const [key, rt] of Object.entries(realtimeServices)) {
|
|
39102
39144
|
try {
|
|
39103
|
-
|
|
39104
|
-
|
|
39105
|
-
await rtWithLifecycle.destroy();
|
|
39145
|
+
if (typeof rt.destroy === "function") {
|
|
39146
|
+
await rt.destroy();
|
|
39106
39147
|
logger.info(`Realtime service "${key}" destroyed`);
|
|
39107
|
-
} else if (typeof
|
|
39108
|
-
await
|
|
39148
|
+
} else if (typeof rt.stopListening === "function") {
|
|
39149
|
+
await rt.stopListening();
|
|
39109
39150
|
logger.info(`Realtime service "${key}" LISTEN client stopped`);
|
|
39110
39151
|
}
|
|
39111
39152
|
} catch (err) {
|
|
@@ -51252,6 +51293,7 @@ export {
|
|
|
51252
51293
|
isAuthAdapter,
|
|
51253
51294
|
isDatabaseAdapter,
|
|
51254
51295
|
isOperationAllowed,
|
|
51296
|
+
isRebaseApiError,
|
|
51255
51297
|
isTransformableImage,
|
|
51256
51298
|
listenWithPortRetry,
|
|
51257
51299
|
loadCronJobsFromDirectory,
|