@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.
- package/dist/common/src/collections/default-collections.d.ts +5 -8
- package/dist/common/src/data/query_builder.d.ts +6 -2
- package/dist/index.es.js +318 -282
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +318 -282
- 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/auth.d.ts +2 -2
- package/dist/types/src/controllers/client.d.ts +25 -40
- package/dist/types/src/controllers/data.d.ts +21 -3
- package/dist/types/src/controllers/data_driver.d.ts +5 -0
- package/dist/types/src/controllers/email.d.ts +2 -0
- package/dist/types/src/types/auth_adapter.d.ts +3 -56
- package/dist/types/src/types/backend.d.ts +2 -2
- package/dist/types/src/types/backend_hooks.d.ts +2 -17
- 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 +9 -7
- package/dist/types/src/types/user_management_delegate.d.ts +16 -53
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -1
- 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/auth/admin-routes.ts +2 -91
- package/src/auth/builtin-auth-adapter.ts +5 -55
- package/src/auth/custom-auth-adapter.ts +1 -1
- package/src/email/smtp-email-service.ts +31 -0
- package/src/email/types.ts +1 -0
- package/src/init.ts +136 -24
- package/src/storage/image-transform.ts +2 -1
- package/test/admin-routes.test.ts +0 -169
- package/test/backend-hooks-admin.test.ts +0 -25
- package/test/custom-auth-adapter.test.ts +2 -10
- package/test/smtp-email-service.test.ts +169 -0
- package/dist/types/src/users/roles.d.ts +0 -14
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
|
29
|
+
* Are auth-related operations currently loading?
|
|
27
30
|
*/
|
|
28
31
|
loading: boolean;
|
|
29
32
|
/**
|
|
30
|
-
*
|
|
33
|
+
* In-memory list of users (used for client-side filtering fallback).
|
|
31
34
|
*/
|
|
32
|
-
users
|
|
35
|
+
users?: USER[];
|
|
33
36
|
/**
|
|
34
|
-
*
|
|
37
|
+
* Error from fetching the users list, if any.
|
|
35
38
|
*/
|
|
36
39
|
usersError?: Error;
|
|
37
40
|
/**
|
|
38
|
-
*
|
|
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
|
|
43
|
+
getUser?: (uid: string) => USER | null;
|
|
43
44
|
/**
|
|
44
|
-
*
|
|
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?: (
|
|
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<
|
|
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`
|
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/
|
|
58
|
-
"@rebasepro/
|
|
59
|
-
"@rebasepro/
|
|
60
|
-
"@rebasepro/
|
|
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)),
|
|
@@ -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
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
193
|
+
// Implicit eq
|
|
79
194
|
let parsedVal: string | number | boolean | null = value;
|
|
80
|
-
if (
|
|
81
|
-
|
|
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
|
-
|
|
215
|
+
const orderByVal = getLastValue(query.orderBy);
|
|
216
|
+
if (orderByVal) {
|
|
102
217
|
try {
|
|
103
|
-
options.orderBy = typeof
|
|
104
|
-
? JSON.parse(
|
|
105
|
-
:
|
|
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
|
|
109
|
-
const [field, direction] =
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
|
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(
|
|
276
|
+
property: String(vectorSearchVal),
|
|
157
277
|
vector: queryVector,
|
|
158
278
|
distance: distanceParam,
|
|
159
279
|
};
|
|
160
280
|
|
|
161
|
-
|
|
162
|
-
|
|
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) */
|