@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.
@@ -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";
@@ -19,6 +19,7 @@ export interface SMTPConfig {
19
19
  user: string;
20
20
  pass: string;
21
21
  };
22
+ name?: string;
22
23
  }
23
24
  /**
24
25
  * Template function for password reset emails
@@ -1,4 +1,4 @@
1
- import { DataDriver, EntityCollection, BackendBootstrapper, BootstrappedAuth, RealtimeProvider, HealthCheckResult, BackendHooks, AuthAdapter, DatabaseAdapter } from "@rebasepro/types";
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(column: keyof M & string, operator: FilterOperator, value: unknown): this;
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(column: keyof M & string, operator: FilterOperator, value: unknown): QueryBuilderInterface<M>;
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, EntityDetailViewConfig } from "./entity_views";
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
- * Customization options for the read-only detail view.
128
- * Only used when `defaultEntityAction` is `"view"`.
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
- detailView?: EntityDetailViewConfig;
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 for customizing the read-only detail view of an entity.
69
- * Only used when `defaultEntityAction` is set to `"view"` on the collection.
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 EntityDetailViewConfig<M extends Record<string, unknown> = Record<string, unknown>> = {
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 rendered below the property display in the detail view.
71
+ * Custom component that replaces the default form.
79
72
  */
80
- Footer?: ComponentRef<EntityDetailViewParams<M>>;
73
+ Builder: ComponentRef<EntityCustomViewParams<M>>;
81
74
  /**
82
- * Completely replace the default detail view with a custom component.
83
- * When set, Header and Footer are ignored.
75
+ * If true, the save/delete action bar is rendered alongside the custom view.
76
+ * Defaults to true.
84
77
  */
85
- Builder?: ComponentRef<EntityDetailViewParams<M>>;
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: Entity<M>;
94
- path: string;
95
- onEditClick: () => void;
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.2.5",
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.2.5",
58
- "@rebasepro/common": "0.2.5",
59
- "@rebasepro/utils": "0.2.5",
60
- "@rebasepro/types": "0.2.5"
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.query();
90
+ const queryDict = c.req.queries();
91
91
  const queryOptions = parseQueryOptions(queryDict);
92
- const searchString = queryDict.searchString as string | undefined;
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.query();
102
+ const queryDict = c.req.queries();
103
103
  const queryOptions = parseQueryOptions(queryDict);
104
- const searchString = queryDict.searchString as string | undefined;
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.query();
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.query();
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: queryDict.searchString as string | undefined
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.query();
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: queryDict.searchString as string | undefined
421
+ searchString
420
422
  });
421
423
  return c.json({
422
424
  data: entities.map(e => this.flattenEntity(e)),