@rebasepro/server-postgresql 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 +93 -213
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +92 -212
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +7 -4
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +2 -1
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +5 -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 +6 -6
- package/src/PostgresBootstrapper.ts +22 -9
- package/src/auth/ensure-tables.ts +30 -28
- package/src/schema/generate-drizzle-schema.ts +2 -6
- package/src/services/EntityFetchService.ts +13 -1
- package/src/utils/drizzle-conditions.ts +40 -5
- package/src/websocket.ts +1 -3
- package/test/relation-pipeline-gaps.test.ts +315 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
-
import {
|
|
2
|
+
import type { EntityCollection } from "@rebasepro/types";
|
|
3
3
|
/**
|
|
4
|
-
* Auto-create auth tables if they don't exist
|
|
5
|
-
*
|
|
4
|
+
* Auto-create auth tables if they don't exist.
|
|
5
|
+
*
|
|
6
|
+
* @param db — Drizzle database instance
|
|
7
|
+
* @param collection — The collection that represents auth users.
|
|
8
|
+
* When omitted, a default `rebase.users` table is created.
|
|
6
9
|
*/
|
|
7
|
-
export declare function ensureAuthTablesExist(db: NodePgDatabase,
|
|
10
|
+
export declare function ensureAuthTablesExist(db: NodePgDatabase, collection?: EntityCollection): Promise<void>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SQL } from "drizzle-orm";
|
|
2
2
|
import { PgTable } from "drizzle-orm/pg-core";
|
|
3
|
-
import { Entity, FilterValues } from "@rebasepro/types";
|
|
3
|
+
import { Entity, FilterValues, LogicalCondition } from "@rebasepro/types";
|
|
4
4
|
import type { VectorSearchParams } from "@rebasepro/types";
|
|
5
5
|
import { RelationService } from "./RelationService";
|
|
6
6
|
import { DrizzleClient } from "../interfaces";
|
|
@@ -104,6 +104,7 @@ export declare class EntityFetchService {
|
|
|
104
104
|
searchString?: string;
|
|
105
105
|
databaseId?: string;
|
|
106
106
|
vectorSearch?: VectorSearchParams;
|
|
107
|
+
logical?: LogicalCondition;
|
|
107
108
|
}): Promise<Entity<M>[]>;
|
|
108
109
|
/**
|
|
109
110
|
* Fallback path used when db.query is unavailable.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SQL } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
3
|
-
import { FilterValues, WhereFilterOp, Relation } from "@rebasepro/types";
|
|
3
|
+
import { FilterValues, WhereFilterOp, Relation, LogicalCondition, FilterCondition } from "@rebasepro/types";
|
|
4
4
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
5
5
|
/** Drizzle dynamic query builder — accepts innerJoin + where chaining */
|
|
6
6
|
export interface DrizzleDynamicQuery {
|
|
@@ -22,6 +22,10 @@ export declare class DrizzleConditionBuilder {
|
|
|
22
22
|
* Build filter conditions from FilterValues
|
|
23
23
|
*/
|
|
24
24
|
static buildFilterConditions<M extends Record<string, unknown>>(filter: FilterValues<Extract<keyof M, string>>, table: PgTable<any>, collectionPath: string): SQL[];
|
|
25
|
+
/**
|
|
26
|
+
* Build logical conditions recursively from LogicalCondition or FilterCondition
|
|
27
|
+
*/
|
|
28
|
+
static buildLogicalConditions(cond: LogicalCondition | FilterCondition, table: PgTable<any>, collectionPath: string): SQL | null;
|
|
25
29
|
/**
|
|
26
30
|
* Build a single filter condition for a specific operator and value
|
|
27
31
|
*/
|
|
@@ -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-postgresql",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -68,11 +68,11 @@
|
|
|
68
68
|
"hono": "^4.12.21",
|
|
69
69
|
"pg": "^8.21.0",
|
|
70
70
|
"ws": "^8.20.1",
|
|
71
|
-
"@rebasepro/
|
|
72
|
-
"@rebasepro/
|
|
73
|
-
"@rebasepro/
|
|
74
|
-
"@rebasepro/
|
|
75
|
-
"@rebasepro/
|
|
71
|
+
"@rebasepro/sdk-generator": "0.4.0",
|
|
72
|
+
"@rebasepro/server-core": "0.4.0",
|
|
73
|
+
"@rebasepro/types": "0.4.0",
|
|
74
|
+
"@rebasepro/common": "0.4.0",
|
|
75
|
+
"@rebasepro/utils": "0.4.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@types/jest": "^29.5.14",
|
|
@@ -180,31 +180,44 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
|
|
|
180
180
|
},
|
|
181
181
|
|
|
182
182
|
async initializeAuth(config: unknown, driverResult: InitializedDriver): Promise<BootstrappedAuth | undefined> {
|
|
183
|
-
const authConfig = config as
|
|
183
|
+
const authConfig = config as Record<string, unknown> | undefined;
|
|
184
184
|
if (!authConfig) return undefined;
|
|
185
185
|
|
|
186
186
|
const internals = driverResult.internals as PostgresDriverInternals;
|
|
187
187
|
const db = internals.db;
|
|
188
188
|
const registry = internals.registry;
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
// Resolve the auth collection from the explicit config.
|
|
191
|
+
// This replaces the old `registry.getTable("users")` magic string lookup.
|
|
192
|
+
const authCollection = authConfig.collection as EntityCollection | undefined;
|
|
193
|
+
|
|
194
|
+
// ensureAuthTablesExist works with the collection abstraction — no Drizzle leakage.
|
|
195
|
+
await ensureAuthTablesExist(db, authCollection);
|
|
191
196
|
|
|
192
197
|
let emailService: EmailService | undefined;
|
|
193
198
|
if (authConfig.email) {
|
|
194
|
-
emailService = createEmailService(authConfig.email);
|
|
199
|
+
emailService = createEmailService(authConfig.email as EmailConfig);
|
|
195
200
|
}
|
|
196
201
|
|
|
197
|
-
|
|
202
|
+
// Resolve the Drizzle table for the internal UserService/AuthRepository.
|
|
203
|
+
// These are internal Postgres-specific services that need the Drizzle table reference.
|
|
204
|
+
const tableName = authCollection
|
|
205
|
+
? ("table" in authCollection && typeof authCollection.table === "string"
|
|
206
|
+
? authCollection.table
|
|
207
|
+
: authCollection.slug)
|
|
208
|
+
: undefined;
|
|
209
|
+
const usersTable = tableName
|
|
210
|
+
? registry.getTable(tableName) as (PgTable & Record<string, AnyPgColumn>) | undefined
|
|
211
|
+
: undefined;
|
|
198
212
|
|
|
199
213
|
let usersSchemaName = "rebase";
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
usersSchemaName = getTableConfig(customUsersTable).schema || "public";
|
|
214
|
+
if (authCollection && "schema" in authCollection && typeof authCollection.schema === "string") {
|
|
215
|
+
usersSchemaName = authCollection.schema;
|
|
203
216
|
}
|
|
204
217
|
|
|
205
218
|
const authTables = createAuthSchema(usersSchemaName) as unknown as AuthSchemaTables;
|
|
206
|
-
if (
|
|
207
|
-
authTables.users =
|
|
219
|
+
if (usersTable) {
|
|
220
|
+
authTables.users = usersTable as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
208
221
|
}
|
|
209
222
|
|
|
210
223
|
const userService = new UserService(db, authTables);
|
|
@@ -1,44 +1,46 @@
|
|
|
1
1
|
import { sql } from "drizzle-orm";
|
|
2
2
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
-
import { getTableConfig, AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
4
|
-
import { getColumnMeta } from "../services/entity-helpers";
|
|
5
|
-
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
6
3
|
import { logger } from "@rebasepro/server-core";
|
|
4
|
+
import type { EntityCollection } from "@rebasepro/types";
|
|
7
5
|
|
|
8
6
|
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
|
-
* Auto-create auth tables if they don't exist
|
|
12
|
-
*
|
|
9
|
+
* Auto-create auth tables if they don't exist.
|
|
10
|
+
*
|
|
11
|
+
* @param db — Drizzle database instance
|
|
12
|
+
* @param collection — The collection that represents auth users.
|
|
13
|
+
* When omitted, a default `rebase.users` table is created.
|
|
13
14
|
*/
|
|
14
|
-
export async function ensureAuthTablesExist(db: NodePgDatabase,
|
|
15
|
+
export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: EntityCollection): Promise<void> {
|
|
15
16
|
logger.info("🔍 Checking auth tables...");
|
|
16
17
|
|
|
17
18
|
try {
|
|
18
|
-
// Resolve dynamic user table name and ID type
|
|
19
|
-
let usersTableName = '"users"';
|
|
19
|
+
// Resolve dynamic user table name and ID type from the collection
|
|
20
|
+
let usersTableName = '"rebase"."users"';
|
|
20
21
|
let userIdType = "TEXT";
|
|
21
|
-
let usersSchema = "
|
|
22
|
-
if (
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
22
|
+
let usersSchema = "rebase";
|
|
23
|
+
if (collection) {
|
|
24
|
+
const rawTable = ("table" in collection && typeof collection.table === "string")
|
|
25
|
+
? collection.table
|
|
26
|
+
: collection.slug;
|
|
27
|
+
usersSchema = ("schema" in collection && typeof collection.schema === "string")
|
|
28
|
+
? collection.schema
|
|
29
|
+
: "public";
|
|
30
|
+
usersTableName = usersSchema === "public"
|
|
31
|
+
? `"${rawTable}"`
|
|
32
|
+
: `"${usersSchema}"."${rawTable}"`;
|
|
33
|
+
|
|
34
|
+
// Derive ID column type from collection properties
|
|
35
|
+
const idProp = collection.properties?.id;
|
|
36
|
+
if (idProp) {
|
|
37
|
+
const isId = ("isId" in idProp) ? (idProp as unknown as Record<string, unknown>).isId : undefined;
|
|
38
|
+
if (isId === "uuid") {
|
|
39
|
+
userIdType = "UUID";
|
|
40
|
+
} else if (isId === "autoincrement") {
|
|
41
|
+
userIdType = "INTEGER";
|
|
41
42
|
}
|
|
43
|
+
// Otherwise keep TEXT as default
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
|
|
@@ -5,7 +5,7 @@ import { pathToFileURL } from "url";
|
|
|
5
5
|
import chokidar from "chokidar";
|
|
6
6
|
import { generateSchema } from "./generate-drizzle-schema-logic";
|
|
7
7
|
import { EntityCollection } from "@rebasepro/types";
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
|
|
10
10
|
// --- Helper Functions ---
|
|
11
11
|
|
|
@@ -90,11 +90,7 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
|
|
|
90
90
|
collections = [];
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
// Map keyed by slug — last-write-wins, so developer collections overwrite defaults
|
|
95
|
-
collections = Array.from(
|
|
96
|
-
new Map([defaultUsersCollection, ...collections].map(c => [c.slug, c])).values()
|
|
97
|
-
);
|
|
93
|
+
|
|
98
94
|
|
|
99
95
|
// Sort collections by slug alphabetically to ensure deterministic schema generation
|
|
100
96
|
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { and, asc, count, desc, eq, getTableName, gt, lt, or, SQL, TableRelationalConfig, TablesRelationalConfig } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
3
|
-
import { Entity, EntityCollection, FilterValues, Relation } from "@rebasepro/types";
|
|
3
|
+
import { Entity, EntityCollection, FilterValues, Relation, LogicalCondition } from "@rebasepro/types";
|
|
4
4
|
import type { VectorSearchParams } from "@rebasepro/types";
|
|
5
5
|
import { resolveCollectionRelations, findRelation, createRelationRef, createRelationRefWithData } from "@rebasepro/common";
|
|
6
6
|
import { DrizzleConditionBuilder } from "../utils/drizzle-conditions";
|
|
@@ -465,6 +465,7 @@ export class EntityFetchService {
|
|
|
465
465
|
offset?: number;
|
|
466
466
|
startAfter?: Record<string, unknown>;
|
|
467
467
|
searchString?: string;
|
|
468
|
+
logical?: LogicalCondition;
|
|
468
469
|
},
|
|
469
470
|
collectionPath: string,
|
|
470
471
|
withConfig?: Record<string, unknown>
|
|
@@ -494,6 +495,11 @@ export class EntityFetchService {
|
|
|
494
495
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
495
496
|
}
|
|
496
497
|
|
|
498
|
+
if (options.logical) {
|
|
499
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
|
|
500
|
+
if (logicalCondition) allConditions.push(logicalCondition);
|
|
501
|
+
}
|
|
502
|
+
|
|
497
503
|
// Cursor-based pagination (startAfter)
|
|
498
504
|
if (options.startAfter) {
|
|
499
505
|
const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options, collectionPath);
|
|
@@ -700,6 +706,7 @@ export class EntityFetchService {
|
|
|
700
706
|
searchString?: string;
|
|
701
707
|
databaseId?: string;
|
|
702
708
|
vectorSearch?: VectorSearchParams;
|
|
709
|
+
logical?: LogicalCondition;
|
|
703
710
|
} = {}
|
|
704
711
|
): Promise<Entity<M>[]> {
|
|
705
712
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
@@ -774,6 +781,11 @@ export class EntityFetchService {
|
|
|
774
781
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
775
782
|
}
|
|
776
783
|
|
|
784
|
+
if (options.logical) {
|
|
785
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
|
|
786
|
+
if (logicalCondition) allConditions.push(logicalCondition);
|
|
787
|
+
}
|
|
788
|
+
|
|
777
789
|
// Vector distance threshold filter
|
|
778
790
|
if (vectorMeta?.filter) {
|
|
779
791
|
allConditions.push(vectorMeta.filter);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { and, eq, or, sql, SQL, ilike, inArray } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn, PgTable, PgVarchar, PgText, PgChar } from "drizzle-orm/pg-core";
|
|
3
|
-
import { FilterValues, WhereFilterOp, Relation, JoinStep } from "@rebasepro/types";
|
|
3
|
+
import { FilterValues, WhereFilterOp, Relation, JoinStep, LogicalCondition, FilterCondition } from "@rebasepro/types";
|
|
4
4
|
import { getColumnName, resolveCollectionRelations } from "@rebasepro/common";
|
|
5
5
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
6
6
|
import { ConditionBuilderStatic } from "../interfaces";
|
|
@@ -37,7 +37,6 @@ export class DrizzleConditionBuilder {
|
|
|
37
37
|
for (const [field, filterParam] of Object.entries(filter)) {
|
|
38
38
|
if (!filterParam) continue;
|
|
39
39
|
|
|
40
|
-
const [op, value] = filterParam as [WhereFilterOp, any];
|
|
41
40
|
let fieldColumn = table[field as keyof typeof table] as AnyPgColumn;
|
|
42
41
|
|
|
43
42
|
if (!fieldColumn) {
|
|
@@ -53,15 +52,51 @@ export class DrizzleConditionBuilder {
|
|
|
53
52
|
continue;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
const paramsList = Array.isArray(filterParam) && filterParam.length > 0 && Array.isArray(filterParam[0])
|
|
56
|
+
? (filterParam as [WhereFilterOp, any][])
|
|
57
|
+
: [filterParam as [WhereFilterOp, any]];
|
|
58
|
+
|
|
59
|
+
for (const [op, value] of paramsList) {
|
|
60
|
+
const condition = this.buildSingleFilterCondition(fieldColumn, op, value);
|
|
61
|
+
if (condition) {
|
|
62
|
+
conditions.push(condition);
|
|
63
|
+
}
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
return conditions;
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Build logical conditions recursively from LogicalCondition or FilterCondition
|
|
72
|
+
*/
|
|
73
|
+
static buildLogicalConditions(
|
|
74
|
+
cond: LogicalCondition | FilterCondition,
|
|
75
|
+
table: PgTable<any>,
|
|
76
|
+
collectionPath: string
|
|
77
|
+
): SQL | null {
|
|
78
|
+
if ("type" in cond) {
|
|
79
|
+
const subSQLs = cond.conditions
|
|
80
|
+
.map(c => this.buildLogicalConditions(c, table, collectionPath))
|
|
81
|
+
.filter((sql): sql is SQL => sql !== null);
|
|
82
|
+
if (subSQLs.length === 0) return null;
|
|
83
|
+
return (cond.type === "or" ? or(...subSQLs) : and(...subSQLs)) ?? null;
|
|
84
|
+
} else {
|
|
85
|
+
let fieldColumn = table[cond.column as keyof typeof table] as AnyPgColumn;
|
|
86
|
+
if (!fieldColumn) {
|
|
87
|
+
const relationKey = `${cond.column}_id`;
|
|
88
|
+
if (relationKey in table) {
|
|
89
|
+
fieldColumn = table[relationKey as keyof typeof table] as AnyPgColumn;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!fieldColumn) {
|
|
93
|
+
console.warn(`Filtering by field '${cond.column}', but it does not exist in table for collection '${collectionPath}'`);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return this.buildSingleFilterCondition(fieldColumn, cond.operator as WhereFilterOp, cond.value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
65
100
|
/**
|
|
66
101
|
* Build a single filter condition for a specific operator and value
|
|
67
102
|
*/
|
package/src/websocket.ts
CHANGED
|
@@ -547,15 +547,13 @@ colors: true }));
|
|
|
547
547
|
}
|
|
548
548
|
break;
|
|
549
549
|
|
|
550
|
-
// Route subscription messages to RealtimeService
|
|
550
|
+
// Route subscription messages, broadcast channels, and presence to RealtimeService
|
|
551
551
|
case "subscribe_collection":
|
|
552
552
|
case "subscribe_entity":
|
|
553
553
|
case "unsubscribe":
|
|
554
|
-
// Broadcast channels
|
|
555
554
|
case "join_channel":
|
|
556
555
|
case "leave_channel":
|
|
557
556
|
case "broadcast":
|
|
558
|
-
// Presence
|
|
559
557
|
case "presence_track":
|
|
560
558
|
case "presence_untrack":
|
|
561
559
|
case "presence_state": {
|