@rebasepro/server-core 0.2.5 → 0.4.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/common/src/data/query_builder.d.ts +6 -2
- package/dist/index.es.js +306 -251
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +306 -251
- package/dist/index.umd.js.map +1 -1
- 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/email/types.d.ts +1 -0
- package/dist/server-core/src/init.d.ts +31 -1
- package/dist/types/src/controllers/data.d.ts +17 -3
- package/dist/types/src/controllers/email.d.ts +2 -0
- package/dist/types/src/types/collections.d.ts +9 -5
- package/dist/types/src/types/entity_views.d.ts +19 -28
- package/dist/types/src/types/properties.d.ts +2 -2
- package/package.json +5 -5
- package/src/api/rest/api-generator.ts +11 -9
- package/src/api/rest/query-parser.ts +184 -63
- package/src/api/types.ts +2 -1
- package/src/email/smtp-email-service.ts +31 -0
- package/src/email/types.ts +1 -0
- package/src/init.ts +123 -29
- package/src/storage/image-transform.ts +2 -1
- package/test/smtp-email-service.test.ts +169 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { LogicalCondition, FilterCondition } from "@rebasepro/types";
|
|
1
2
|
import { QueryOptions } from "../types";
|
|
2
3
|
/**
|
|
3
4
|
* Map PostgREST-style operators to Rebase WhereFilterOp
|
|
4
5
|
*/
|
|
5
6
|
export declare function mapOperator(op: string): string | null;
|
|
7
|
+
export declare function parseLogicalString(str: string): FilterCondition | LogicalCondition;
|
|
6
8
|
/**
|
|
7
9
|
* Parse query parameters into QueryOptions
|
|
8
10
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityCollection, VectorSearchParams } from "@rebasepro/types";
|
|
1
|
+
import { EntityCollection, VectorSearchParams, LogicalCondition } 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";
|
|
@@ -75,6 +75,7 @@ export interface QueryOptions {
|
|
|
75
75
|
limit?: number;
|
|
76
76
|
offset?: number;
|
|
77
77
|
where?: Record<string, unknown>;
|
|
78
|
+
logical?: LogicalCondition;
|
|
78
79
|
orderBy?: Array<{
|
|
79
80
|
field: string;
|
|
80
81
|
direction: "asc" | "desc";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AuthAdapter, BackendBootstrapper, BackendHooks, BootstrappedAuth, DatabaseAdapter, DataDriver, EntityCollection, HealthCheckResult, RealtimeProvider, SecurityRule } from "@rebasepro/types";
|
|
2
2
|
import { BackendCollectionRegistry } from "./collections/BackendCollectionRegistry";
|
|
3
3
|
import { DriverRegistry } from "./services/driver-registry";
|
|
4
4
|
import { Server } from "http";
|
|
@@ -9,6 +9,22 @@ import { EmailConfig } from "./email";
|
|
|
9
9
|
import type { OAuthProvider } from "./auth/interfaces";
|
|
10
10
|
import type { AuthHooks } from "./auth/auth-hooks";
|
|
11
11
|
export interface RebaseAuthConfig {
|
|
12
|
+
/**
|
|
13
|
+
* The collection that represents auth users.
|
|
14
|
+
*
|
|
15
|
+
* When provided, this collection's underlying database table is used
|
|
16
|
+
* for all auth operations (login, registration, password reset, etc.).
|
|
17
|
+
*
|
|
18
|
+
* Import the built-in default:
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { defaultUsersCollection } from "@rebasepro/common";
|
|
21
|
+
* auth: { collection: defaultUsersCollection, jwtSecret: "..." }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Or pass your own collection with the required auth fields
|
|
25
|
+
* (email, passwordHash, displayName, etc.).
|
|
26
|
+
*/
|
|
27
|
+
collection?: EntityCollection;
|
|
12
28
|
jwtSecret?: string;
|
|
13
29
|
accessExpiresIn?: string;
|
|
14
30
|
refreshExpiresIn?: string;
|
|
@@ -147,6 +163,20 @@ export interface RebaseBackendConfig {
|
|
|
147
163
|
*/
|
|
148
164
|
storage?: BackendStorageConfig | StorageController | Record<string, BackendStorageConfig | StorageController>;
|
|
149
165
|
history?: unknown;
|
|
166
|
+
/**
|
|
167
|
+
* Default security rules applied to any collection that does not define
|
|
168
|
+
* its own `securityRules`. Opt-in — if not set, collections without
|
|
169
|
+
* explicit rules remain unrestricted (beyond `requireAuth`).
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```ts
|
|
173
|
+
* defaultSecurityRules: [
|
|
174
|
+
* { operation: "select", access: "public" },
|
|
175
|
+
* { operations: ["insert", "update", "delete"], roles: ["admin"] }
|
|
176
|
+
* ]
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
defaultSecurityRules?: SecurityRule[];
|
|
150
180
|
enableSwagger?: boolean;
|
|
151
181
|
functionsDir?: string;
|
|
152
182
|
cronsDir?: string;
|
|
@@ -16,7 +16,17 @@ import { Entity, EntityValues } from "../types/entities";
|
|
|
16
16
|
*
|
|
17
17
|
* @group Data
|
|
18
18
|
*/
|
|
19
|
-
export type WhereFieldValue = string | number | boolean | null | [WhereFilterOpShort, any];
|
|
19
|
+
export type WhereFieldValue = string | number | boolean | null | [WhereFilterOpShort, any] | [WhereFilterOpShort, any][];
|
|
20
|
+
export type WhereValue<T> = T | T[] | null;
|
|
21
|
+
export interface LogicalCondition {
|
|
22
|
+
type: "and" | "or";
|
|
23
|
+
conditions: (FilterCondition | LogicalCondition)[];
|
|
24
|
+
}
|
|
25
|
+
export interface FilterCondition {
|
|
26
|
+
column: string;
|
|
27
|
+
operator: FilterOperator;
|
|
28
|
+
value: unknown;
|
|
29
|
+
}
|
|
20
30
|
/** Short operator strings accepted in the tuple syntax. */
|
|
21
31
|
export type WhereFilterOpShort = "==" | "!=" | ">" | ">=" | "<" | "<=" | "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in" | "nin" | "not-in" | "array-contains" | "array-contains-any" | "cs" | "csa";
|
|
22
32
|
export interface FindParams {
|
|
@@ -51,6 +61,8 @@ export interface FindParams {
|
|
|
51
61
|
* ```
|
|
52
62
|
*/
|
|
53
63
|
where?: Record<string, WhereFieldValue>;
|
|
64
|
+
/** Logical grouping conditions (AND/OR) */
|
|
65
|
+
logical?: LogicalCondition;
|
|
54
66
|
/**
|
|
55
67
|
* Sort order. Format: "field:direction".
|
|
56
68
|
* @example "created_at:desc", "name:asc"
|
|
@@ -82,7 +94,8 @@ export type FilterOperator = WhereFilterOpShort;
|
|
|
82
94
|
* @group Data
|
|
83
95
|
*/
|
|
84
96
|
export interface QueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
85
|
-
where
|
|
97
|
+
where<K extends keyof M & string>(column: K, operator: FilterOperator, value: WhereValue<M[K]>): this;
|
|
98
|
+
where(logicalCondition: LogicalCondition): this;
|
|
86
99
|
orderBy(column: keyof M & string, ascending?: "asc" | "desc"): this;
|
|
87
100
|
limit(count: number): this;
|
|
88
101
|
offset(count: number): this;
|
|
@@ -143,7 +156,8 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
|
|
|
143
156
|
* Count the number of records matching the given filter.
|
|
144
157
|
*/
|
|
145
158
|
count?(params?: FindParams): Promise<number>;
|
|
146
|
-
where
|
|
159
|
+
where<K extends keyof M & string>(column: K, operator: FilterOperator, value: WhereValue<M[K]>): QueryBuilderInterface<M>;
|
|
160
|
+
where(logicalCondition: LogicalCondition): QueryBuilderInterface<M>;
|
|
147
161
|
orderBy(column: keyof M & string, ascending?: "asc" | "desc"): QueryBuilderInterface<M>;
|
|
148
162
|
limit(count: number): QueryBuilderInterface<M>;
|
|
149
163
|
offset(count: number): QueryBuilderInterface<M>;
|
|
@@ -31,4 +31,6 @@ export interface EmailService {
|
|
|
31
31
|
send(options: EmailSendOptions): Promise<void>;
|
|
32
32
|
/** Returns `true` when the service has valid credentials / is ready to send. */
|
|
33
33
|
isConfigured(): boolean;
|
|
34
|
+
/** Verify connection/credentials with the email provider. */
|
|
35
|
+
verifyConnection?(): Promise<boolean>;
|
|
34
36
|
}
|
|
@@ -7,7 +7,7 @@ import type { EntityOverrides } from "./entity_overrides";
|
|
|
7
7
|
import type { User } from "../users";
|
|
8
8
|
import type { RebaseContext } from "../rebase_context";
|
|
9
9
|
import type { Relation } from "./relations";
|
|
10
|
-
import type { EntityCustomView,
|
|
10
|
+
import type { EntityCustomView, FormViewConfig } from "./entity_views";
|
|
11
11
|
import type { EntityAction } from "./entity_actions";
|
|
12
12
|
import type { ComponentRef } from "./component_ref";
|
|
13
13
|
/**
|
|
@@ -124,10 +124,14 @@ export interface BaseEntityCollection<M extends Record<string, unknown> = Record
|
|
|
124
124
|
*/
|
|
125
125
|
defaultEntityAction?: "view" | "edit";
|
|
126
126
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
127
|
+
* Replace the default entity form with a custom component.
|
|
128
|
+
* The Builder receives the same props as entity view tabs
|
|
129
|
+
* (entity, formContext, collection, etc.) and has full control over the UI.
|
|
130
|
+
*
|
|
131
|
+
* Works in both edit mode and read-only mode (when `defaultEntityAction`
|
|
132
|
+
* is `"view"`). In read-only mode, `formContext.readOnly` will be `true`.
|
|
129
133
|
*/
|
|
130
|
-
|
|
134
|
+
formView?: FormViewConfig;
|
|
131
135
|
/**
|
|
132
136
|
* Prevent default actions from being displayed or executed on this collection.
|
|
133
137
|
*/
|
|
@@ -566,7 +570,7 @@ export type WhereFilterOp = "<" | "<=" | "==" | "!=" | ">=" | ">" | "array-conta
|
|
|
566
570
|
*
|
|
567
571
|
* @group Models
|
|
568
572
|
*/
|
|
569
|
-
export type FilterValues<Key extends string> = Partial<Record<Key, [WhereFilterOp, unknown]>>;
|
|
573
|
+
export type FilterValues<Key extends string> = Partial<Record<Key, [WhereFilterOp, unknown] | [WhereFilterOp, unknown][]>>;
|
|
570
574
|
/**
|
|
571
575
|
* A pre-defined filter preset for quick access in the collection toolbar.
|
|
572
576
|
* Users can select a preset to instantly apply a set of filters and
|
|
@@ -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
|
}
|
|
@@ -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>;
|
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.4.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/client": "0.
|
|
58
|
-
"@rebasepro/common": "0.
|
|
59
|
-
"@rebasepro/utils": "0.
|
|
60
|
-
"@rebasepro/types": "0.
|
|
57
|
+
"@rebasepro/client": "0.4.0",
|
|
58
|
+
"@rebasepro/common": "0.4.0",
|
|
59
|
+
"@rebasepro/utils": "0.4.0",
|
|
60
|
+
"@rebasepro/types": "0.4.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/jest": "^29.5.14",
|
|
@@ -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);
|
|
@@ -388,13 +388,14 @@ entityId };
|
|
|
388
388
|
|
|
389
389
|
if (parsed.entityId === "count") {
|
|
390
390
|
// GET /parent/:parentId/child/count — count child entities
|
|
391
|
-
const queryDict = c.req.
|
|
391
|
+
const queryDict = c.req.queries();
|
|
392
392
|
const queryOptions = parseQueryOptions(queryDict);
|
|
393
|
+
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : undefined;
|
|
393
394
|
|
|
394
395
|
const total = driver.countEntities ? await driver.countEntities({
|
|
395
396
|
path: parsed.collectionPath,
|
|
396
397
|
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
397
|
-
searchString
|
|
398
|
+
searchString
|
|
398
399
|
}) : 0;
|
|
399
400
|
|
|
400
401
|
return c.json({ count: total });
|
|
@@ -408,15 +409,16 @@ entityId };
|
|
|
408
409
|
return c.json(this.flattenEntity(entity));
|
|
409
410
|
} else {
|
|
410
411
|
// GET /parent/:parentId/child — list entities
|
|
411
|
-
const queryDict = c.req.
|
|
412
|
+
const queryDict = c.req.queries();
|
|
412
413
|
const queryOptions = parseQueryOptions(queryDict);
|
|
414
|
+
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : undefined;
|
|
413
415
|
const entities = await driver.fetchCollection({
|
|
414
416
|
path: parsed.collectionPath,
|
|
415
417
|
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
416
418
|
limit: queryOptions.limit,
|
|
417
419
|
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
418
420
|
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
419
|
-
searchString
|
|
421
|
+
searchString
|
|
420
422
|
});
|
|
421
423
|
return c.json({
|
|
422
424
|
data: entities.map(e => this.flattenEntity(e)),
|