@rebasepro/server-core 0.3.0 → 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.
Files changed (40) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +5 -8
  2. package/dist/common/src/data/query_builder.d.ts +6 -2
  3. package/dist/index.es.js +318 -282
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/index.umd.js +318 -282
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/server-core/src/api/rest/query-parser.d.ts +2 -0
  8. package/dist/server-core/src/api/types.d.ts +2 -1
  9. package/dist/server-core/src/email/types.d.ts +1 -0
  10. package/dist/server-core/src/init.d.ts +31 -1
  11. package/dist/types/src/controllers/auth.d.ts +2 -2
  12. package/dist/types/src/controllers/client.d.ts +25 -40
  13. package/dist/types/src/controllers/data.d.ts +21 -3
  14. package/dist/types/src/controllers/data_driver.d.ts +5 -0
  15. package/dist/types/src/controllers/email.d.ts +2 -0
  16. package/dist/types/src/types/auth_adapter.d.ts +3 -56
  17. package/dist/types/src/types/backend.d.ts +2 -2
  18. package/dist/types/src/types/backend_hooks.d.ts +2 -17
  19. package/dist/types/src/types/collections.d.ts +9 -5
  20. package/dist/types/src/types/entity_views.d.ts +19 -28
  21. package/dist/types/src/types/properties.d.ts +9 -7
  22. package/dist/types/src/types/user_management_delegate.d.ts +16 -53
  23. package/dist/types/src/users/index.d.ts +0 -1
  24. package/dist/types/src/users/user.d.ts +0 -1
  25. package/package.json +5 -5
  26. package/src/api/rest/api-generator.ts +11 -9
  27. package/src/api/rest/query-parser.ts +184 -63
  28. package/src/api/types.ts +2 -1
  29. package/src/auth/admin-routes.ts +2 -91
  30. package/src/auth/builtin-auth-adapter.ts +5 -55
  31. package/src/auth/custom-auth-adapter.ts +1 -1
  32. package/src/email/smtp-email-service.ts +31 -0
  33. package/src/email/types.ts +1 -0
  34. package/src/init.ts +136 -24
  35. package/src/storage/image-transform.ts +2 -1
  36. package/test/admin-routes.test.ts +0 -169
  37. package/test/backend-hooks-admin.test.ts +0 -25
  38. package/test/custom-auth-adapter.test.ts +2 -10
  39. package/test/smtp-email-service.test.ts +169 -0
  40. package/dist/types/src/users/roles.d.ts +0 -14
@@ -1,4 +1,4 @@
1
- import { Role, User } from "../users";
1
+ import type { User } from "../users";
2
2
  /**
3
3
  * Result of creating a new user via admin flow.
4
4
  * Contains the created user plus information about how credentials were delivered.
@@ -15,56 +15,46 @@ export interface UserCreationResult<USER extends User = User> {
15
15
  temporaryPassword?: string;
16
16
  }
17
17
  /**
18
- * Delegate to manage users, roles, and their permissions.
19
- * This interface allows the CMS to be completely agnostic of the underlying
20
- * authentication provider or backend.
18
+ * Delegate to manage auth-specific user operations.
19
+ *
20
+ * This interface allows the CMS to be agnostic of the underlying
21
+ * authentication provider or backend. User/role CRUD is now handled
22
+ * by the collection system; this delegate only exposes auth-specific
23
+ * operations (password hashing, invitations, bootstrap).
21
24
  *
22
25
  * @group Models
23
26
  */
24
27
  export interface UserManagementDelegate<USER extends User = User> {
25
28
  /**
26
- * Are the users and roles currently being fetched?
29
+ * Are auth-related operations currently loading?
27
30
  */
28
31
  loading: boolean;
29
32
  /**
30
- * List of users managed by the CMS.
33
+ * In-memory list of users (used for client-side filtering fallback).
31
34
  */
32
- users: USER[];
35
+ users?: USER[];
33
36
  /**
34
- * Optional error if users failed to load.
37
+ * Error from fetching the users list, if any.
35
38
  */
36
39
  usersError?: Error;
37
40
  /**
38
- * Function to get a user by its uid. This is used to show
39
- * user information when assigning ownership of an entity.
40
- * @param uid
41
+ * Look up a single user by UID from the in-memory cache.
41
42
  */
42
- getUser: (uid: string) => USER | null;
43
+ getUser?: (uid: string) => USER | null;
43
44
  /**
44
- * Search users with server-side pagination.
45
- * When provided, the CMS will use this for the users table
46
- * instead of loading all users into memory.
45
+ * Server-side user search with pagination.
47
46
  */
48
- searchUsers?: (options: {
47
+ searchUsers?: (params: {
49
48
  search?: string;
50
49
  limit?: number;
51
50
  offset?: number;
52
- orderBy?: string;
53
- orderDir?: "asc" | "desc";
54
- roleId?: string;
55
51
  }) => Promise<{
56
52
  users: USER[];
57
53
  total: number;
58
54
  }>;
59
- /**
60
- * Save a user (create or update)
61
- * @param user
62
- */
63
- saveUser?: (user: USER) => Promise<USER>;
64
55
  /**
65
56
  * Create a new user with invitation/password generation support.
66
57
  * Returns additional info about how the credentials were delivered.
67
- * Falls back to saveUser if not provided.
68
58
  */
69
59
  createUser?: (user: USER) => Promise<UserCreationResult<USER>>;
70
60
  /**
@@ -73,42 +63,15 @@ export interface UserManagementDelegate<USER extends User = User> {
73
63
  * or a flag indicating an email invitation was sent.
74
64
  */
75
65
  resetPassword?: (user: USER) => Promise<UserCreationResult<USER>>;
76
- /**
77
- * Delete a user
78
- * @param user
79
- */
80
- deleteUser?: (user: USER) => Promise<void>;
81
- /**
82
- * List of roles defined in the CMS.
83
- */
84
- roles?: Role[];
85
- /**
86
- * Optional error if roles failed to load.
87
- */
88
- rolesError?: Error;
89
- /**
90
- * Save a role (create or update)
91
- * @param role
92
- */
93
- saveRole?: (role: Role) => Promise<void>;
94
- /**
95
- * Delete a role
96
- * @param role
97
- */
98
- deleteRole?: (role: Role) => Promise<void>;
99
66
  /**
100
67
  * Is the currently logged in user an admin?
101
68
  */
102
69
  isAdmin?: boolean;
103
- /**
104
- * If true, the UI will allow the user to create the default roles (admin, editor, viewer).
105
- */
106
- allowDefaultRolesCreation?: boolean;
107
70
  /**
108
71
  * Optionally define roles for a given user. This is useful when the roles
109
72
  * are coming from a separate provider than the one issuing the tokens.
110
73
  */
111
- defineRolesFor?: (user: USER) => Promise<Role[] | undefined> | Role[] | undefined;
74
+ defineRolesFor?: (user: USER) => Promise<string[] | undefined> | string[] | undefined;
112
75
  /**
113
76
  * Whether any admin users exist. Used by the bootstrap banner to decide
114
77
  * whether to prompt. Populated via a lightweight check (e.g. `limit=1`
@@ -1,2 +1 @@
1
1
  export * from "./user";
2
- export * from "./roles";
@@ -35,7 +35,6 @@ export type User = {
35
35
  readonly isAnonymous: boolean;
36
36
  /**
37
37
  * Role IDs assigned to this user (e.g. ["admin", "editor"]).
38
- * These are plain string IDs — use the UserManagementDelegate to look up full Role objects.
39
38
  */
40
39
  roles?: string[];
41
40
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/server-core",
3
3
  "type": "module",
4
- "version": "0.3.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/common": "0.3.0",
58
- "@rebasepro/types": "0.3.0",
59
- "@rebasepro/client": "0.3.0",
60
- "@rebasepro/utils": "0.3.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.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)),
@@ -1,4 +1,4 @@
1
- import type { VectorSearchParams } from "@rebasepro/types";
1
+ import type { VectorSearchParams, LogicalCondition, FilterCondition } from "@rebasepro/types";
2
2
  import { QueryOptions } from "../types";
3
3
 
4
4
  /**
@@ -20,6 +20,86 @@ export function mapOperator(op: string): string | null {
20
20
  }
21
21
  }
22
22
 
23
+ function getLastValue(val: unknown): unknown {
24
+ if (Array.isArray(val)) {
25
+ return val[val.length - 1];
26
+ }
27
+ return val;
28
+ }
29
+
30
+ export function parseLogicalString(str: string): FilterCondition | LogicalCondition {
31
+ str = str.trim();
32
+ if (str.startsWith("or(") && str.endsWith(")")) {
33
+ const inner = str.slice(3, -1);
34
+ return { type: "or", conditions: parseLogicalList(inner) };
35
+ }
36
+ if (str.startsWith("and(") && str.endsWith(")")) {
37
+ const inner = str.slice(4, -1);
38
+ return { type: "and", conditions: parseLogicalList(inner) };
39
+ }
40
+
41
+ // It's a leaf condition: field.op.val
42
+ const firstDot = str.indexOf(".");
43
+ if (firstDot === -1) {
44
+ return { column: str, operator: "==", value: true };
45
+ }
46
+ const field = str.substring(0, firstDot);
47
+ const rest = str.substring(firstDot + 1);
48
+ const secondDot = rest.indexOf(".");
49
+ let op = "eq";
50
+ let valStr = rest;
51
+ if (secondDot !== -1) {
52
+ op = rest.substring(0, secondDot);
53
+ valStr = rest.substring(secondDot + 1);
54
+ } else {
55
+ op = "eq";
56
+ valStr = rest;
57
+ }
58
+
59
+ const rebaseOp = (mapOperator(op) || "==") as FilterCondition["operator"];
60
+ let parsedVal: unknown = valStr;
61
+ if (valStr === "true") parsedVal = true;
62
+ else if (valStr === "false") parsedVal = false;
63
+ else if (valStr === "null") parsedVal = null;
64
+ else if (!isNaN(Number(valStr)) && valStr.trim() !== "") parsedVal = Number(valStr);
65
+ else if (valStr.startsWith("(")) {
66
+ const arrayContent = valStr.endsWith(")") ? valStr.slice(1, -1) : valStr.slice(1);
67
+ parsedVal = arrayContent.split(",").map(v => {
68
+ const trimmed = v.trim();
69
+ if (!isNaN(Number(trimmed)) && trimmed !== "") return Number(trimmed);
70
+ if (trimmed === "true") return true;
71
+ if (trimmed === "false") return false;
72
+ if (trimmed === "null") return null;
73
+ return trimmed;
74
+ });
75
+ }
76
+
77
+ return { column: field, operator: rebaseOp, value: parsedVal };
78
+ }
79
+
80
+ function parseLogicalList(str: string): (FilterCondition | LogicalCondition)[] {
81
+ const list: (FilterCondition | LogicalCondition)[] = [];
82
+ let depth = 0;
83
+ let current = "";
84
+
85
+ for (let i = 0; i < str.length; i++) {
86
+ const char = str[i];
87
+ if (char === "(") depth++;
88
+ if (char === ")") depth--;
89
+
90
+ if (char === "," && depth === 0) {
91
+ list.push(parseLogicalString(current));
92
+ current = "";
93
+ } else {
94
+ current += char;
95
+ }
96
+ }
97
+ if (current) {
98
+ list.push(parseLogicalString(current));
99
+ }
100
+ return list;
101
+ }
102
+
23
103
  /**
24
104
  * Parse query parameters into QueryOptions
25
105
  */
@@ -27,10 +107,15 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
27
107
  const options: QueryOptions = {};
28
108
 
29
109
  // Pagination
30
- if (query.limit) options.limit = parseInt(String(query.limit));
31
- if (query.offset) options.offset = parseInt(String(query.offset));
32
- if (query.page) {
33
- const page = parseInt(String(query.page));
110
+ const limitVal = getLastValue(query.limit);
111
+ if (limitVal) options.limit = parseInt(String(limitVal));
112
+
113
+ const offsetVal = getLastValue(query.offset);
114
+ if (offsetVal) options.offset = parseInt(String(offsetVal));
115
+
116
+ const pageVal = getLastValue(query.page);
117
+ if (pageVal) {
118
+ const page = parseInt(String(pageVal));
34
119
  const limit = options.limit || 20;
35
120
  options.offset = (page - 1) * limit;
36
121
  }
@@ -38,59 +123,88 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
38
123
  // Filtering
39
124
  options.where = {};
40
125
 
126
+ // Handle logical conditions
127
+ const orVal = getLastValue(query.or);
128
+ const andVal = getLastValue(query.and);
129
+ if (orVal) {
130
+ let orStr = String(orVal).trim();
131
+ if (orStr.startsWith("(") && orStr.endsWith(")")) {
132
+ orStr = orStr.slice(1, -1);
133
+ }
134
+ options.logical = {
135
+ type: "or",
136
+ conditions: parseLogicalList(orStr)
137
+ };
138
+ } else if (andVal) {
139
+ let andStr = String(andVal).trim();
140
+ if (andStr.startsWith("(") && andStr.endsWith(")")) {
141
+ andStr = andStr.slice(1, -1);
142
+ }
143
+ options.logical = {
144
+ type: "and",
145
+ conditions: parseLogicalList(andStr)
146
+ };
147
+ }
41
148
 
42
149
  // PostgREST-style filtering: ?field=op.value
43
- const reservedQueryKeys = ["limit", "offset", "page", "orderBy", "include", "fields", "searchString", "vector_search", "vector", "vector_distance", "vector_threshold"];
150
+ const reservedQueryKeys = ["limit", "offset", "page", "orderBy", "include", "fields", "searchString", "vector_search", "vector", "vector_distance", "vector_threshold", "or", "and"];
44
151
  for (const [key, rawValue] of Object.entries(query)) {
45
152
  if (reservedQueryKeys.includes(key)) continue;
46
153
 
47
- const value = Array.isArray(rawValue) ? rawValue[rawValue.length - 1] : rawValue;
48
-
49
- if (typeof value === "string") {
50
- const parts = value.split(".");
51
- if (parts.length >= 2) {
52
- const op = parts[0];
53
- const val = parts.slice(1).join(".");
54
- const rebaseOp = mapOperator(op);
55
-
56
- if (rebaseOp) {
57
- let parsedVal: string | number | boolean | null | (string | number | boolean | null)[] = val;
58
- // Attempt to parse primitive types or arrays
59
- if (val === "true") parsedVal = true;
60
- else if (val === "false") parsedVal = false;
61
- else if (val === "null") parsedVal = null;
62
- else if (!isNaN(Number(val)) && val.trim() !== "") parsedVal = Number(val);
63
- else if (val.startsWith("(")) {
64
- // Array for 'in' or 'not-in' ops (e.g. (1,2,3) or (a,b,c))
65
- const arrayContent = val.endsWith(")") ? val.slice(1, -1) : val.slice(1);
66
- parsedVal = arrayContent.split(",").map(v => {
67
- const trimmed = v.trim();
68
- if (!isNaN(Number(trimmed)) && trimmed !== "") return Number(trimmed);
69
- if (trimmed === "true") return true;
70
- if (trimmed === "false") return false;
71
- if (trimmed === "null") return null;
72
- return trimmed;
73
- });
74
- }
154
+ const rawValues = Array.isArray(rawValue) ? rawValue : [rawValue];
155
+ const conditions: [string, unknown][] = [];
156
+
157
+ for (const value of rawValues) {
158
+ if (typeof value === "string") {
159
+ const parts = value.split(".");
160
+ if (parts.length >= 2) {
161
+ const op = parts[0];
162
+ const val = parts.slice(1).join(".");
163
+ const rebaseOp = mapOperator(op);
75
164
 
76
- options.where[key] = [rebaseOp, parsedVal];
165
+ if (rebaseOp) {
166
+ let parsedVal: string | number | boolean | null | (string | number | boolean | null)[] = val;
167
+ // Attempt to parse primitive types or arrays
168
+ if (val === "true") parsedVal = true;
169
+ else if (val === "false") parsedVal = false;
170
+ else if (val === "null") parsedVal = null;
171
+ else if (!isNaN(Number(val)) && val.trim() !== "") parsedVal = Number(val);
172
+ else if (val.startsWith("(")) {
173
+ // Array for 'in' or 'not-in' ops (e.g. (1,2,3) or (a,b,c))
174
+ const arrayContent = val.endsWith(")") ? val.slice(1, -1) : val.slice(1);
175
+ parsedVal = arrayContent.split(",").map(v => {
176
+ const trimmed = v.trim();
177
+ if (!isNaN(Number(trimmed)) && trimmed !== "") return Number(trimmed);
178
+ if (trimmed === "true") return true;
179
+ if (trimmed === "false") return false;
180
+ if (trimmed === "null") return null;
181
+ return trimmed;
182
+ });
183
+ }
184
+
185
+ conditions.push([rebaseOp, parsedVal]);
186
+ } else {
187
+ // Fallback: assume implicit eq if the dot wasn't an operator (e.g. email or float)
188
+ let parsedVal: string | number | boolean | null = value;
189
+ if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
190
+ conditions.push(["==", parsedVal]);
191
+ }
77
192
  } else {
78
- // Fallback: assume implicit eq if the dot wasn't an operator (e.g. email or float)
193
+ // Implicit eq
79
194
  let parsedVal: string | number | boolean | null = value;
80
- if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
81
- options.where[key] = ["==", parsedVal];
195
+ if (value === "true") parsedVal = true;
196
+ else if (value === "false") parsedVal = false;
197
+ else if (value === "null") parsedVal = null;
198
+ else if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
199
+
200
+ conditions.push(["==", parsedVal]);
82
201
  }
83
- } else {
84
- // Implicit eq
85
- let parsedVal: string | number | boolean | null = value;
86
- if (value === "true") parsedVal = true;
87
- else if (value === "false") parsedVal = false;
88
- else if (value === "null") parsedVal = null;
89
- else if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
90
-
91
- options.where[key] = ["==", parsedVal];
92
202
  }
93
203
  }
204
+
205
+ if (conditions.length > 0) {
206
+ options.where[key] = conditions.length === 1 ? conditions[0] : conditions;
207
+ }
94
208
  }
95
209
 
96
210
  if (Object.keys(options.where).length === 0) {
@@ -98,15 +212,16 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
98
212
  }
99
213
 
100
214
  // Sorting
101
- if (query.orderBy) {
215
+ const orderByVal = getLastValue(query.orderBy);
216
+ if (orderByVal) {
102
217
  try {
103
- options.orderBy = typeof query.orderBy === "string"
104
- ? JSON.parse(query.orderBy)
105
- : query.orderBy;
218
+ options.orderBy = typeof orderByVal === "string"
219
+ ? JSON.parse(orderByVal)
220
+ : orderByVal;
106
221
  } catch {
107
222
  // Try simple format: "field:direction"
108
- if (typeof query.orderBy === "string") {
109
- const [field, direction] = query.orderBy.split(":");
223
+ if (typeof orderByVal === "string") {
224
+ const [field, direction] = orderByVal.split(":");
110
225
  const dir = (direction === "desc" ? "desc" : "asc") as "asc" | "desc";
111
226
  options.orderBy = [
112
227
  {
@@ -119,8 +234,9 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
119
234
  }
120
235
 
121
236
  // Relation includes
122
- if (query.include) {
123
- const includeStr = String(query.include).trim();
237
+ const includeVal = getLastValue(query.include);
238
+ if (includeVal) {
239
+ const includeStr = String(includeVal).trim();
124
240
  if (includeStr === "*") {
125
241
  options.include = ["*"];
126
242
  } else {
@@ -129,14 +245,17 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
129
245
  }
130
246
 
131
247
  // Field selection
132
- if (query.fields) {
133
- const fieldsStr = String(query.fields).trim();
248
+ const fieldsVal = getLastValue(query.fields);
249
+ if (fieldsVal) {
250
+ const fieldsStr = String(fieldsVal).trim();
134
251
  options.fields = fieldsStr.split(",").map(s => s.trim()).filter(Boolean);
135
252
  }
136
253
 
137
254
  // Vector similarity search
138
- if (query.vector_search && query.vector) {
139
- const vectorStr = String(query.vector);
255
+ const vectorSearchVal = getLastValue(query.vector_search);
256
+ const vectorVal = getLastValue(query.vector);
257
+ if (vectorSearchVal && vectorVal) {
258
+ const vectorStr = String(vectorVal);
140
259
  let queryVector: number[];
141
260
  try {
142
261
  queryVector = JSON.parse(vectorStr) as number[];
@@ -147,19 +266,21 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
147
266
  throw new Error("Invalid vector format. Expected JSON array of numbers, e.g. [0.1,0.2,0.3]");
148
267
  }
149
268
 
150
- const distanceParam = query.vector_distance ? String(query.vector_distance) : "cosine";
269
+ const distanceParamVal = getLastValue(query.vector_distance);
270
+ const distanceParam = distanceParamVal ? String(distanceParamVal) : "cosine";
151
271
  if (distanceParam !== "cosine" && distanceParam !== "l2" && distanceParam !== "inner_product") {
152
272
  throw new Error(`Invalid vector_distance: ${distanceParam}. Expected: cosine, l2, or inner_product`);
153
273
  }
154
274
 
155
275
  const vectorSearch: VectorSearchParams = {
156
- property: String(query.vector_search),
276
+ property: String(vectorSearchVal),
157
277
  vector: queryVector,
158
278
  distance: distanceParam,
159
279
  };
160
280
 
161
- if (query.vector_threshold) {
162
- const threshold = parseFloat(String(query.vector_threshold));
281
+ const thresholdVal = getLastValue(query.vector_threshold);
282
+ if (thresholdVal) {
283
+ const threshold = parseFloat(String(thresholdVal));
163
284
  if (isNaN(threshold)) {
164
285
  throw new Error("Invalid vector_threshold. Expected a number.");
165
286
  }
package/src/api/types.ts CHANGED
@@ -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";
@@ -77,6 +77,7 @@ export interface QueryOptions {
77
77
  limit?: number;
78
78
  offset?: number;
79
79
  where?: Record<string, unknown>;
80
+ logical?: LogicalCondition;
80
81
  orderBy?: Array<{ field: string; direction: "asc" | "desc" }>;
81
82
  include?: string[];
82
83
  /** Columns to return in the response (field-level selection) */