postgresdk 0.15.6 → 0.16.1
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/README.md +124 -2
- package/dist/cli.js +399 -29
- package/dist/index.js +399 -29
- package/dist/introspect.d.ts +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1051,6 +1051,8 @@ function postgresTypeToTsType(column, enums) {
|
|
|
1051
1051
|
case "_int8":
|
|
1052
1052
|
case "_integer":
|
|
1053
1053
|
return "number[]";
|
|
1054
|
+
case "vector":
|
|
1055
|
+
return "number[]";
|
|
1054
1056
|
default:
|
|
1055
1057
|
return "string";
|
|
1056
1058
|
}
|
|
@@ -1204,6 +1206,8 @@ function postgresTypeToJsonType(pgType, enums) {
|
|
|
1204
1206
|
case "_int8":
|
|
1205
1207
|
case "_integer":
|
|
1206
1208
|
return "number[]";
|
|
1209
|
+
case "vector":
|
|
1210
|
+
return "number[]";
|
|
1207
1211
|
default:
|
|
1208
1212
|
return "string";
|
|
1209
1213
|
}
|
|
@@ -1397,6 +1401,12 @@ function generateUnifiedContractMarkdown(contract) {
|
|
|
1397
1401
|
lines.push("| `$ilike` | Pattern match (case-insensitive) | `{ email: { $ilike: '%@GMAIL%' } }` | String |");
|
|
1398
1402
|
lines.push("| `$is` | IS NULL | `{ deleted_at: { $is: null } }` | Nullable fields |");
|
|
1399
1403
|
lines.push("| `$isNot` | IS NOT NULL | `{ created_by: { $isNot: null } }` | Nullable fields |");
|
|
1404
|
+
lines.push("| `$jsonbContains` | JSONB contains | `{ metadata: { $jsonbContains: { tags: ['premium'] } } }` | JSONB/JSON |");
|
|
1405
|
+
lines.push("| `$jsonbContainedBy` | JSONB contained by | `{ metadata: { $jsonbContainedBy: {...} } }` | JSONB/JSON |");
|
|
1406
|
+
lines.push("| `$jsonbHasKey` | JSONB has key | `{ settings: { $jsonbHasKey: 'theme' } }` | JSONB/JSON |");
|
|
1407
|
+
lines.push("| `$jsonbHasAnyKeys` | JSONB has any keys | `{ settings: { $jsonbHasAnyKeys: ['dark', 'light'] } }` | JSONB/JSON |");
|
|
1408
|
+
lines.push("| `$jsonbHasAllKeys` | JSONB has all keys | `{ config: { $jsonbHasAllKeys: ['api', 'db'] } }` | JSONB/JSON |");
|
|
1409
|
+
lines.push("| `$jsonbPath` | JSONB nested value | `{ meta: { $jsonbPath: { path: ['user', 'age'], operator: '$gte', value: 18 } } }` | JSONB/JSON |");
|
|
1400
1410
|
lines.push("");
|
|
1401
1411
|
lines.push("### Logical Operators");
|
|
1402
1412
|
lines.push("");
|
|
@@ -1505,6 +1515,43 @@ function generateUnifiedContractMarkdown(contract) {
|
|
|
1505
1515
|
lines.push("");
|
|
1506
1516
|
lines.push("**Note:** Column names are validated by Zod schemas. Only valid table columns are accepted, preventing SQL injection.");
|
|
1507
1517
|
lines.push("");
|
|
1518
|
+
lines.push("## Vector Search");
|
|
1519
|
+
lines.push("");
|
|
1520
|
+
lines.push("For tables with `vector` columns (requires pgvector extension), use the `vector` parameter for similarity search:");
|
|
1521
|
+
lines.push("");
|
|
1522
|
+
lines.push("```typescript");
|
|
1523
|
+
lines.push("// Basic similarity search");
|
|
1524
|
+
lines.push("const results = await sdk.embeddings.list({");
|
|
1525
|
+
lines.push(" vector: {");
|
|
1526
|
+
lines.push(" field: 'embedding',");
|
|
1527
|
+
lines.push(" query: [0.1, 0.2, 0.3, ...], // Your embedding vector");
|
|
1528
|
+
lines.push(" metric: 'cosine' // 'cosine' (default), 'l2', or 'inner'");
|
|
1529
|
+
lines.push(" },");
|
|
1530
|
+
lines.push(" limit: 10");
|
|
1531
|
+
lines.push("});");
|
|
1532
|
+
lines.push("");
|
|
1533
|
+
lines.push("// Results include _distance field");
|
|
1534
|
+
lines.push("results.data[0]._distance; // Similarity distance");
|
|
1535
|
+
lines.push("");
|
|
1536
|
+
lines.push("// Distance threshold filtering");
|
|
1537
|
+
lines.push("const closeMatches = await sdk.embeddings.list({");
|
|
1538
|
+
lines.push(" vector: {");
|
|
1539
|
+
lines.push(" field: 'embedding',");
|
|
1540
|
+
lines.push(" query: queryVector,");
|
|
1541
|
+
lines.push(" maxDistance: 0.5 // Only return results within this distance");
|
|
1542
|
+
lines.push(" }");
|
|
1543
|
+
lines.push("});");
|
|
1544
|
+
lines.push("");
|
|
1545
|
+
lines.push("// Hybrid search: vector + WHERE filters");
|
|
1546
|
+
lines.push("const filtered = await sdk.embeddings.list({");
|
|
1547
|
+
lines.push(" vector: { field: 'embedding', query: queryVector },");
|
|
1548
|
+
lines.push(" where: {");
|
|
1549
|
+
lines.push(" status: 'published',");
|
|
1550
|
+
lines.push(" embedding: { $isNot: null }");
|
|
1551
|
+
lines.push(" }");
|
|
1552
|
+
lines.push("});");
|
|
1553
|
+
lines.push("```");
|
|
1554
|
+
lines.push("");
|
|
1508
1555
|
lines.push("## Resources");
|
|
1509
1556
|
lines.push("");
|
|
1510
1557
|
for (const resource of contract.resources) {
|
|
@@ -2510,19 +2557,35 @@ async function introspect(connectionString, schema) {
|
|
|
2510
2557
|
for (const r of tablesRows.rows)
|
|
2511
2558
|
ensureTable(tables, r.table);
|
|
2512
2559
|
const colsRows = await pg.query(`
|
|
2513
|
-
SELECT
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2560
|
+
SELECT
|
|
2561
|
+
c.table_name,
|
|
2562
|
+
c.column_name,
|
|
2563
|
+
c.is_nullable,
|
|
2564
|
+
c.udt_name,
|
|
2565
|
+
c.data_type,
|
|
2566
|
+
c.column_default,
|
|
2567
|
+
a.atttypmod
|
|
2568
|
+
FROM information_schema.columns c
|
|
2569
|
+
LEFT JOIN pg_catalog.pg_class cl ON cl.relname = c.table_name
|
|
2570
|
+
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cl.relnamespace AND n.nspname = c.table_schema
|
|
2571
|
+
LEFT JOIN pg_catalog.pg_attribute a ON a.attrelid = cl.oid AND a.attname = c.column_name
|
|
2572
|
+
WHERE c.table_schema = $1
|
|
2573
|
+
ORDER BY c.table_name, c.ordinal_position
|
|
2517
2574
|
`, [schema]);
|
|
2518
2575
|
for (const r of colsRows.rows) {
|
|
2519
2576
|
const t = ensureTable(tables, r.table_name);
|
|
2520
|
-
|
|
2577
|
+
const pgType = (r.udt_name ?? r.data_type).toLowerCase();
|
|
2578
|
+
const col = {
|
|
2521
2579
|
name: r.column_name,
|
|
2522
|
-
pgType
|
|
2580
|
+
pgType,
|
|
2523
2581
|
nullable: r.is_nullable === "YES",
|
|
2524
2582
|
hasDefault: r.column_default != null
|
|
2525
|
-
}
|
|
2583
|
+
};
|
|
2584
|
+
const isVectorType = pgType === "vector" || pgType === "halfvec" || pgType === "sparsevec" || pgType === "bit";
|
|
2585
|
+
if (isVectorType && r.atttypmod != null && r.atttypmod !== -1) {
|
|
2586
|
+
col.vectorDimension = r.atttypmod - 4;
|
|
2587
|
+
}
|
|
2588
|
+
t.columns.push(col);
|
|
2526
2589
|
}
|
|
2527
2590
|
const pkRows = await pg.query(`
|
|
2528
2591
|
SELECT
|
|
@@ -2799,6 +2862,7 @@ function emitParamsZod(table, graph) {
|
|
|
2799
2862
|
const includeSpecSchema = `z.any()`;
|
|
2800
2863
|
const pkSchema = hasCompositePk ? `z.object({ ${safePk.map((col) => `${col}: z.string().min(1)`).join(", ")} })` : `z.string().min(1)`;
|
|
2801
2864
|
return `import { z } from "zod";
|
|
2865
|
+
import { VectorSearchParamsSchema } from "./shared.js";
|
|
2802
2866
|
|
|
2803
2867
|
// Schema for primary key parameters
|
|
2804
2868
|
export const ${Type}PkSchema = ${pkSchema};
|
|
@@ -2809,6 +2873,7 @@ export const ${Type}ListParamsSchema = z.object({
|
|
|
2809
2873
|
limit: z.number().int().positive().max(1000).optional(),
|
|
2810
2874
|
offset: z.number().int().nonnegative().optional(),
|
|
2811
2875
|
where: z.any().optional(),
|
|
2876
|
+
vector: VectorSearchParamsSchema.optional(),
|
|
2812
2877
|
orderBy: z.enum([${columnNames}]).optional(),
|
|
2813
2878
|
order: z.enum(["asc", "desc"]).optional()
|
|
2814
2879
|
}).strict();
|
|
@@ -2835,7 +2900,16 @@ export const PaginationParamsSchema = z.object({
|
|
|
2835
2900
|
offset: z.number().int().nonnegative().optional()
|
|
2836
2901
|
}).strict();
|
|
2837
2902
|
|
|
2903
|
+
// Shared vector search schema (used across all tables)
|
|
2904
|
+
export const VectorSearchParamsSchema = z.object({
|
|
2905
|
+
field: z.string().min(1),
|
|
2906
|
+
query: z.array(z.number()),
|
|
2907
|
+
metric: z.enum(["cosine", "l2", "inner"]).optional(),
|
|
2908
|
+
maxDistance: z.number().nonnegative().optional()
|
|
2909
|
+
}).strict();
|
|
2910
|
+
|
|
2838
2911
|
export type PaginationParams = z.infer<typeof PaginationParamsSchema>;
|
|
2912
|
+
export type VectorSearchParams = z.infer<typeof VectorSearchParamsSchema>;
|
|
2839
2913
|
`;
|
|
2840
2914
|
}
|
|
2841
2915
|
|
|
@@ -2904,7 +2978,13 @@ const listSchema = z.object({
|
|
|
2904
2978
|
limit: z.number().int().positive().max(1000).optional(),
|
|
2905
2979
|
offset: z.number().int().min(0).optional(),
|
|
2906
2980
|
orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
|
|
2907
|
-
order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional()
|
|
2981
|
+
order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional(),
|
|
2982
|
+
vector: z.object({
|
|
2983
|
+
field: z.string(),
|
|
2984
|
+
query: z.array(z.number()),
|
|
2985
|
+
metric: z.enum(["cosine", "l2", "inner"]).optional(),
|
|
2986
|
+
maxDistance: z.number().optional()
|
|
2987
|
+
}).optional()
|
|
2908
2988
|
});
|
|
2909
2989
|
|
|
2910
2990
|
/**
|
|
@@ -3086,9 +3166,14 @@ ${hasAuth ? `
|
|
|
3086
3166
|
// src/emit-client.ts
|
|
3087
3167
|
init_utils();
|
|
3088
3168
|
init_emit_include_methods();
|
|
3169
|
+
function isVectorType(pgType) {
|
|
3170
|
+
const t = pgType.toLowerCase();
|
|
3171
|
+
return t === "vector" || t === "halfvec" || t === "sparsevec" || t === "bit";
|
|
3172
|
+
}
|
|
3089
3173
|
function emitClient(table, graph, opts, model) {
|
|
3090
3174
|
const Type = pascal(table.name);
|
|
3091
3175
|
const ext = opts.useJsExtensions ? ".js" : "";
|
|
3176
|
+
const hasVectorColumns = table.columns.some((c) => isVectorType(c.pgType));
|
|
3092
3177
|
const pkCols = Array.isArray(table.pk) ? table.pk : table.pk ? [table.pk] : [];
|
|
3093
3178
|
const safePk = pkCols.length ? pkCols : ["id"];
|
|
3094
3179
|
const hasCompositePk = safePk.length > 1;
|
|
@@ -3168,6 +3253,13 @@ ${typeImports}
|
|
|
3168
3253
|
${otherTableImports.join(`
|
|
3169
3254
|
`)}
|
|
3170
3255
|
|
|
3256
|
+
/**
|
|
3257
|
+
* Helper type to merge JSONB type overrides into base types
|
|
3258
|
+
* @example
|
|
3259
|
+
* type UserWithMetadata = MergeJsonb<SelectUser, { metadata: { tags: string[] } }>;
|
|
3260
|
+
*/
|
|
3261
|
+
type MergeJsonb<TBase, TJsonb> = Omit<TBase, keyof TJsonb> & TJsonb;
|
|
3262
|
+
|
|
3171
3263
|
/**
|
|
3172
3264
|
* Client for ${table.name} table operations
|
|
3173
3265
|
*/
|
|
@@ -3179,8 +3271,20 @@ export class ${Type}Client extends BaseClient {
|
|
|
3179
3271
|
* @param data - The data to insert
|
|
3180
3272
|
* @returns The created record
|
|
3181
3273
|
*/
|
|
3182
|
-
async create(data: Insert${Type}): Promise<Select${Type}
|
|
3183
|
-
|
|
3274
|
+
async create(data: Insert${Type}): Promise<Select${Type}>;
|
|
3275
|
+
/**
|
|
3276
|
+
* Create a new ${table.name} record with JSONB type overrides
|
|
3277
|
+
* @param data - The data to insert
|
|
3278
|
+
* @returns The created record with typed JSONB fields
|
|
3279
|
+
* @example
|
|
3280
|
+
* type Metadata = { tags: string[]; prefs: { theme: 'light' | 'dark' } };
|
|
3281
|
+
* const user = await client.create<{ metadata: Metadata }>({ name: 'Alice', metadata: { tags: [], prefs: { theme: 'light' } } });
|
|
3282
|
+
*/
|
|
3283
|
+
async create<TJsonb extends Partial<Select${Type}>>(
|
|
3284
|
+
data: MergeJsonb<Insert${Type}, TJsonb>
|
|
3285
|
+
): Promise<MergeJsonb<Select${Type}, TJsonb>>;
|
|
3286
|
+
async create(data: any): Promise<any> {
|
|
3287
|
+
return this.post<any>(this.resource, data);
|
|
3184
3288
|
}
|
|
3185
3289
|
|
|
3186
3290
|
/**
|
|
@@ -3188,9 +3292,18 @@ export class ${Type}Client extends BaseClient {
|
|
|
3188
3292
|
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3189
3293
|
* @returns The record if found, null otherwise
|
|
3190
3294
|
*/
|
|
3191
|
-
async getByPk(pk: ${pkType}): Promise<Select${Type} | null
|
|
3295
|
+
async getByPk(pk: ${pkType}): Promise<Select${Type} | null>;
|
|
3296
|
+
/**
|
|
3297
|
+
* Get a ${table.name} record by primary key with JSONB type overrides
|
|
3298
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3299
|
+
* @returns The record with typed JSONB fields if found, null otherwise
|
|
3300
|
+
*/
|
|
3301
|
+
async getByPk<TJsonb extends Partial<Select${Type}>>(
|
|
3302
|
+
pk: ${pkType}
|
|
3303
|
+
): Promise<MergeJsonb<Select${Type}, TJsonb> | null>;
|
|
3304
|
+
async getByPk(pk: ${pkType}): Promise<any> {
|
|
3192
3305
|
const path = ${pkPathExpr};
|
|
3193
|
-
return this.get<
|
|
3306
|
+
return this.get<any>(\`\${this.resource}/\${path}\`);
|
|
3194
3307
|
}
|
|
3195
3308
|
|
|
3196
3309
|
/**
|
|
@@ -3211,8 +3324,48 @@ export class ${Type}Client extends BaseClient {
|
|
|
3211
3324
|
where?: Where<Select${Type}>;
|
|
3212
3325
|
orderBy?: string | string[];
|
|
3213
3326
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3214
|
-
}): Promise<PaginatedResponse<Select${Type}
|
|
3215
|
-
|
|
3327
|
+
}): Promise<PaginatedResponse<Select${Type}>>;${hasVectorColumns ? `
|
|
3328
|
+
/**
|
|
3329
|
+
* List ${table.name} records with vector similarity search
|
|
3330
|
+
* @param params - Query parameters with vector search enabled
|
|
3331
|
+
* @param params.vector - Vector similarity search configuration
|
|
3332
|
+
* @returns Paginated results with _distance field included
|
|
3333
|
+
*/
|
|
3334
|
+
async list(params: {
|
|
3335
|
+
include?: any;
|
|
3336
|
+
limit?: number;
|
|
3337
|
+
offset?: number;
|
|
3338
|
+
where?: Where<Select${Type}>;
|
|
3339
|
+
vector: {
|
|
3340
|
+
field: string;
|
|
3341
|
+
query: number[];
|
|
3342
|
+
metric?: "cosine" | "l2" | "inner";
|
|
3343
|
+
maxDistance?: number;
|
|
3344
|
+
};
|
|
3345
|
+
orderBy?: string | string[];
|
|
3346
|
+
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3347
|
+
}): Promise<PaginatedResponse<Select${Type} & { _distance: number }>>;` : ""}
|
|
3348
|
+
/**
|
|
3349
|
+
* List ${table.name} records with pagination and filtering, with JSONB type overrides
|
|
3350
|
+
* @param params - Query parameters with typed JSONB fields in where clause
|
|
3351
|
+
* @returns Paginated results with typed JSONB fields
|
|
3352
|
+
*/
|
|
3353
|
+
async list<TJsonb extends Partial<Select${Type}>>(params?: {
|
|
3354
|
+
include?: any;
|
|
3355
|
+
limit?: number;
|
|
3356
|
+
offset?: number;
|
|
3357
|
+
where?: Where<MergeJsonb<Select${Type}, TJsonb>>;${hasVectorColumns ? `
|
|
3358
|
+
vector?: {
|
|
3359
|
+
field: string;
|
|
3360
|
+
query: number[];
|
|
3361
|
+
metric?: "cosine" | "l2" | "inner";
|
|
3362
|
+
maxDistance?: number;
|
|
3363
|
+
};` : ""}
|
|
3364
|
+
orderBy?: string | string[];
|
|
3365
|
+
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3366
|
+
}): Promise<PaginatedResponse<MergeJsonb<Select${Type}, TJsonb>>>;
|
|
3367
|
+
async list(params?: any): Promise<any> {
|
|
3368
|
+
return this.post<any>(\`\${this.resource}/list\`, params ?? {});
|
|
3216
3369
|
}
|
|
3217
3370
|
|
|
3218
3371
|
/**
|
|
@@ -3221,9 +3374,20 @@ export class ${Type}Client extends BaseClient {
|
|
|
3221
3374
|
* @param patch - Partial data to update
|
|
3222
3375
|
* @returns The updated record if found, null otherwise
|
|
3223
3376
|
*/
|
|
3224
|
-
async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null
|
|
3377
|
+
async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null>;
|
|
3378
|
+
/**
|
|
3379
|
+
* Update a ${table.name} record by primary key with JSONB type overrides
|
|
3380
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3381
|
+
* @param patch - Partial data to update with typed JSONB fields
|
|
3382
|
+
* @returns The updated record with typed JSONB fields if found, null otherwise
|
|
3383
|
+
*/
|
|
3384
|
+
async update<TJsonb extends Partial<Select${Type}>>(
|
|
3385
|
+
pk: ${pkType},
|
|
3386
|
+
patch: MergeJsonb<Update${Type}, TJsonb>
|
|
3387
|
+
): Promise<MergeJsonb<Select${Type}, TJsonb> | null>;
|
|
3388
|
+
async update(pk: ${pkType}, patch: any): Promise<any> {
|
|
3225
3389
|
const path = ${pkPathExpr};
|
|
3226
|
-
return this.patch<
|
|
3390
|
+
return this.patch<any>(\`\${this.resource}/\${path}\`, patch);
|
|
3227
3391
|
}
|
|
3228
3392
|
|
|
3229
3393
|
/**
|
|
@@ -3231,9 +3395,18 @@ export class ${Type}Client extends BaseClient {
|
|
|
3231
3395
|
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3232
3396
|
* @returns The deleted record if found, null otherwise
|
|
3233
3397
|
*/
|
|
3234
|
-
async delete(pk: ${pkType}): Promise<Select${Type} | null
|
|
3398
|
+
async delete(pk: ${pkType}): Promise<Select${Type} | null>;
|
|
3399
|
+
/**
|
|
3400
|
+
* Delete a ${table.name} record by primary key with JSONB type overrides
|
|
3401
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3402
|
+
* @returns The deleted record with typed JSONB fields if found, null otherwise
|
|
3403
|
+
*/
|
|
3404
|
+
async delete<TJsonb extends Partial<Select${Type}>>(
|
|
3405
|
+
pk: ${pkType}
|
|
3406
|
+
): Promise<MergeJsonb<Select${Type}, TJsonb> | null>;
|
|
3407
|
+
async delete(pk: ${pkType}): Promise<any> {
|
|
3235
3408
|
const path = ${pkPathExpr};
|
|
3236
|
-
return this.del<
|
|
3409
|
+
return this.del<any>(\`\${this.resource}/\${path}\`);
|
|
3237
3410
|
}
|
|
3238
3411
|
${includeMethodsCode}}
|
|
3239
3412
|
`;
|
|
@@ -3904,6 +4077,25 @@ function emitWhereTypes() {
|
|
|
3904
4077
|
* To make changes, modify your schema or configuration and regenerate.
|
|
3905
4078
|
*/
|
|
3906
4079
|
|
|
4080
|
+
/**
|
|
4081
|
+
* Deep partial type for JSONB contains operator
|
|
4082
|
+
*/
|
|
4083
|
+
type DeepPartial<T> = T extends object ? {
|
|
4084
|
+
[P in keyof T]?: DeepPartial<T[P]>;
|
|
4085
|
+
} : T;
|
|
4086
|
+
|
|
4087
|
+
/**
|
|
4088
|
+
* JSONB path query configuration
|
|
4089
|
+
*/
|
|
4090
|
+
type JsonbPathQuery = {
|
|
4091
|
+
/** Array of keys to traverse (e.g., ['user', 'preferences', 'theme']) */
|
|
4092
|
+
path: string[];
|
|
4093
|
+
/** Operator to apply to the value at the path (defaults to '$eq') */
|
|
4094
|
+
operator?: '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$like' | '$ilike';
|
|
4095
|
+
/** Value to compare against */
|
|
4096
|
+
value: any;
|
|
4097
|
+
};
|
|
4098
|
+
|
|
3907
4099
|
/**
|
|
3908
4100
|
* WHERE clause operators for filtering
|
|
3909
4101
|
*/
|
|
@@ -3932,6 +4124,20 @@ export type WhereOperator<T> = {
|
|
|
3932
4124
|
$is?: null;
|
|
3933
4125
|
/** IS NOT NULL */
|
|
3934
4126
|
$isNot?: null;
|
|
4127
|
+
|
|
4128
|
+
// JSONB operators (only available for object/unknown types)
|
|
4129
|
+
/** JSONB contains (@>) - check if column contains the specified JSON structure */
|
|
4130
|
+
$jsonbContains?: T extends object | unknown ? (unknown extends T ? any : DeepPartial<T>) : never;
|
|
4131
|
+
/** JSONB contained by (<@) - check if column is contained by the specified JSON */
|
|
4132
|
+
$jsonbContainedBy?: T extends object | unknown ? any : never;
|
|
4133
|
+
/** JSONB has key (?) - check if top-level key exists */
|
|
4134
|
+
$jsonbHasKey?: T extends object | unknown ? string : never;
|
|
4135
|
+
/** JSONB has any keys (?|) - check if any of the specified keys exist */
|
|
4136
|
+
$jsonbHasAnyKeys?: T extends object | unknown ? string[] : never;
|
|
4137
|
+
/** JSONB has all keys (?&) - check if all of the specified keys exist */
|
|
4138
|
+
$jsonbHasAllKeys?: T extends object | unknown ? string[] : never;
|
|
4139
|
+
/** JSONB path query - query nested values. For multiple paths on same column, use $and */
|
|
4140
|
+
$jsonbPath?: T extends object | unknown ? JsonbPathQuery : never;
|
|
3935
4141
|
};
|
|
3936
4142
|
|
|
3937
4143
|
/**
|
|
@@ -4565,6 +4771,100 @@ function buildWhereClause(
|
|
|
4565
4771
|
whereParts.push(\`"\${key}" IS NOT NULL\`);
|
|
4566
4772
|
}
|
|
4567
4773
|
break;
|
|
4774
|
+
case '$jsonbContains':
|
|
4775
|
+
whereParts.push(\`"\${key}" @> $\${paramIndex}\`);
|
|
4776
|
+
whereParams.push(JSON.stringify(opValue));
|
|
4777
|
+
paramIndex++;
|
|
4778
|
+
break;
|
|
4779
|
+
case '$jsonbContainedBy':
|
|
4780
|
+
whereParts.push(\`"\${key}" <@ $\${paramIndex}\`);
|
|
4781
|
+
whereParams.push(JSON.stringify(opValue));
|
|
4782
|
+
paramIndex++;
|
|
4783
|
+
break;
|
|
4784
|
+
case '$jsonbHasKey':
|
|
4785
|
+
whereParts.push(\`"\${key}" ? $\${paramIndex}\`);
|
|
4786
|
+
whereParams.push(opValue);
|
|
4787
|
+
paramIndex++;
|
|
4788
|
+
break;
|
|
4789
|
+
case '$jsonbHasAnyKeys':
|
|
4790
|
+
if (Array.isArray(opValue) && opValue.length > 0) {
|
|
4791
|
+
whereParts.push(\`"\${key}" ?| $\${paramIndex}\`);
|
|
4792
|
+
whereParams.push(opValue);
|
|
4793
|
+
paramIndex++;
|
|
4794
|
+
}
|
|
4795
|
+
break;
|
|
4796
|
+
case '$jsonbHasAllKeys':
|
|
4797
|
+
if (Array.isArray(opValue) && opValue.length > 0) {
|
|
4798
|
+
whereParts.push(\`"\${key}" ?& $\${paramIndex}\`);
|
|
4799
|
+
whereParams.push(opValue);
|
|
4800
|
+
paramIndex++;
|
|
4801
|
+
}
|
|
4802
|
+
break;
|
|
4803
|
+
case '$jsonbPath':
|
|
4804
|
+
const pathConfig = opValue;
|
|
4805
|
+
const pathKeys = pathConfig.path;
|
|
4806
|
+
const pathOperator = pathConfig.operator || '$eq';
|
|
4807
|
+
const pathValue = pathConfig.value;
|
|
4808
|
+
|
|
4809
|
+
if (!Array.isArray(pathKeys) || pathKeys.length === 0) {
|
|
4810
|
+
break;
|
|
4811
|
+
}
|
|
4812
|
+
|
|
4813
|
+
// Build path accessor: metadata->'user'->'preferences'->>'theme'
|
|
4814
|
+
// Use -> for all keys except the last one, use ->> for the last to get text
|
|
4815
|
+
const pathParts = pathKeys.slice(0, -1);
|
|
4816
|
+
const lastKey = pathKeys[pathKeys.length - 1];
|
|
4817
|
+
|
|
4818
|
+
let pathExpr = \`"\${key}"\`;
|
|
4819
|
+
for (const part of pathParts) {
|
|
4820
|
+
pathExpr += \`->'\${part}'\`;
|
|
4821
|
+
}
|
|
4822
|
+
pathExpr += \`->>'\${lastKey}'\`;
|
|
4823
|
+
|
|
4824
|
+
// Apply the operator
|
|
4825
|
+
switch (pathOperator) {
|
|
4826
|
+
case '$eq':
|
|
4827
|
+
whereParts.push(\`\${pathExpr} = $\${paramIndex}\`);
|
|
4828
|
+
whereParams.push(String(pathValue));
|
|
4829
|
+
paramIndex++;
|
|
4830
|
+
break;
|
|
4831
|
+
case '$ne':
|
|
4832
|
+
whereParts.push(\`\${pathExpr} != $\${paramIndex}\`);
|
|
4833
|
+
whereParams.push(String(pathValue));
|
|
4834
|
+
paramIndex++;
|
|
4835
|
+
break;
|
|
4836
|
+
case '$gt':
|
|
4837
|
+
whereParts.push(\`(\${pathExpr})::numeric > $\${paramIndex}\`);
|
|
4838
|
+
whereParams.push(pathValue);
|
|
4839
|
+
paramIndex++;
|
|
4840
|
+
break;
|
|
4841
|
+
case '$gte':
|
|
4842
|
+
whereParts.push(\`(\${pathExpr})::numeric >= $\${paramIndex}\`);
|
|
4843
|
+
whereParams.push(pathValue);
|
|
4844
|
+
paramIndex++;
|
|
4845
|
+
break;
|
|
4846
|
+
case '$lt':
|
|
4847
|
+
whereParts.push(\`(\${pathExpr})::numeric < $\${paramIndex}\`);
|
|
4848
|
+
whereParams.push(pathValue);
|
|
4849
|
+
paramIndex++;
|
|
4850
|
+
break;
|
|
4851
|
+
case '$lte':
|
|
4852
|
+
whereParts.push(\`(\${pathExpr})::numeric <= $\${paramIndex}\`);
|
|
4853
|
+
whereParams.push(pathValue);
|
|
4854
|
+
paramIndex++;
|
|
4855
|
+
break;
|
|
4856
|
+
case '$like':
|
|
4857
|
+
whereParts.push(\`\${pathExpr} LIKE $\${paramIndex}\`);
|
|
4858
|
+
whereParams.push(pathValue);
|
|
4859
|
+
paramIndex++;
|
|
4860
|
+
break;
|
|
4861
|
+
case '$ilike':
|
|
4862
|
+
whereParts.push(\`\${pathExpr} ILIKE $\${paramIndex}\`);
|
|
4863
|
+
whereParams.push(pathValue);
|
|
4864
|
+
paramIndex++;
|
|
4865
|
+
break;
|
|
4866
|
+
}
|
|
4867
|
+
break;
|
|
4568
4868
|
}
|
|
4569
4869
|
}
|
|
4570
4870
|
} else if (value === null) {
|
|
@@ -4620,17 +4920,51 @@ function buildWhereClause(
|
|
|
4620
4920
|
}
|
|
4621
4921
|
|
|
4622
4922
|
/**
|
|
4623
|
-
*
|
|
4923
|
+
* Get distance operator for vector similarity search
|
|
4924
|
+
*/
|
|
4925
|
+
function getVectorDistanceOperator(metric?: string): string {
|
|
4926
|
+
switch (metric) {
|
|
4927
|
+
case "l2":
|
|
4928
|
+
return "<->";
|
|
4929
|
+
case "inner":
|
|
4930
|
+
return "<#>";
|
|
4931
|
+
case "cosine":
|
|
4932
|
+
default:
|
|
4933
|
+
return "<=>";
|
|
4934
|
+
}
|
|
4935
|
+
}
|
|
4936
|
+
|
|
4937
|
+
/**
|
|
4938
|
+
* LIST operation - Get multiple records with optional filters and vector search
|
|
4624
4939
|
*/
|
|
4625
4940
|
export async function listRecords(
|
|
4626
4941
|
ctx: OperationContext,
|
|
4627
|
-
params: {
|
|
4942
|
+
params: {
|
|
4943
|
+
where?: any;
|
|
4944
|
+
limit?: number;
|
|
4945
|
+
offset?: number;
|
|
4946
|
+
include?: any;
|
|
4947
|
+
orderBy?: string | string[];
|
|
4948
|
+
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4949
|
+
vector?: {
|
|
4950
|
+
field: string;
|
|
4951
|
+
query: number[];
|
|
4952
|
+
metric?: "cosine" | "l2" | "inner";
|
|
4953
|
+
maxDistance?: number;
|
|
4954
|
+
};
|
|
4955
|
+
}
|
|
4628
4956
|
): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
4629
4957
|
try {
|
|
4630
|
-
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order } = params;
|
|
4958
|
+
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector } = params;
|
|
4959
|
+
|
|
4960
|
+
// Get distance operator if vector search
|
|
4961
|
+
const distanceOp = vector ? getVectorDistanceOperator(vector.metric) : "";
|
|
4962
|
+
|
|
4963
|
+
// Add vector to params array if present
|
|
4964
|
+
const queryParams: any[] = vector ? [JSON.stringify(vector.query)] : [];
|
|
4631
4965
|
|
|
4632
|
-
// Build WHERE clause
|
|
4633
|
-
let paramIndex = 1;
|
|
4966
|
+
// Build WHERE clause for SELECT/UPDATE queries (with vector as $1 if present)
|
|
4967
|
+
let paramIndex = vector ? 2 : 1;
|
|
4634
4968
|
const whereParts: string[] = [];
|
|
4635
4969
|
let whereParams: any[] = [];
|
|
4636
4970
|
|
|
@@ -4649,11 +4983,47 @@ export async function listRecords(
|
|
|
4649
4983
|
}
|
|
4650
4984
|
}
|
|
4651
4985
|
|
|
4986
|
+
// Add vector distance threshold filter if specified
|
|
4987
|
+
if (vector?.maxDistance !== undefined) {
|
|
4988
|
+
whereParts.push(\`("\${vector.field}" \${distanceOp} ($1)::vector) < \${vector.maxDistance}\`);
|
|
4989
|
+
}
|
|
4990
|
+
|
|
4652
4991
|
const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
|
|
4653
4992
|
|
|
4993
|
+
// Build WHERE clause for COUNT query (may need different param indices)
|
|
4994
|
+
let countWhereSQL = whereSQL;
|
|
4995
|
+
let countParams = whereParams;
|
|
4996
|
+
|
|
4997
|
+
if (vector && vector.maxDistance === undefined && whereParams.length > 0) {
|
|
4998
|
+
// COUNT query doesn't use vector, so rebuild WHERE without vector offset
|
|
4999
|
+
const countWhereParts: string[] = [];
|
|
5000
|
+
if (ctx.softDeleteColumn) {
|
|
5001
|
+
countWhereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
|
|
5002
|
+
}
|
|
5003
|
+
if (whereClause) {
|
|
5004
|
+
const result = buildWhereClause(whereClause, 1); // Start at $1 for count
|
|
5005
|
+
if (result.sql) {
|
|
5006
|
+
countWhereParts.push(result.sql);
|
|
5007
|
+
countParams = result.params;
|
|
5008
|
+
}
|
|
5009
|
+
}
|
|
5010
|
+
countWhereSQL = countWhereParts.length > 0 ? \`WHERE \${countWhereParts.join(" AND ")}\` : "";
|
|
5011
|
+
} else if (vector?.maxDistance !== undefined) {
|
|
5012
|
+
// COUNT query includes vector for maxDistance filter
|
|
5013
|
+
countParams = [...queryParams, ...whereParams];
|
|
5014
|
+
}
|
|
5015
|
+
|
|
5016
|
+
// Build SELECT clause
|
|
5017
|
+
const selectClause = vector
|
|
5018
|
+
? \`*, ("\${vector.field}" \${distanceOp} ($1)::vector) AS _distance\`
|
|
5019
|
+
: "*";
|
|
5020
|
+
|
|
4654
5021
|
// Build ORDER BY clause
|
|
4655
5022
|
let orderBySQL = "";
|
|
4656
|
-
if (
|
|
5023
|
+
if (vector) {
|
|
5024
|
+
// For vector search, always order by distance
|
|
5025
|
+
orderBySQL = \`ORDER BY "\${vector.field}" \${distanceOp} ($1)::vector\`;
|
|
5026
|
+
} else if (orderBy) {
|
|
4657
5027
|
const columns = Array.isArray(orderBy) ? orderBy : [orderBy];
|
|
4658
5028
|
const directions = Array.isArray(order) ? order : (order ? Array(columns.length).fill(order) : Array(columns.length).fill("asc"));
|
|
4659
5029
|
|
|
@@ -4668,16 +5038,16 @@ export async function listRecords(
|
|
|
4668
5038
|
// Add limit and offset params
|
|
4669
5039
|
const limitParam = \`$\${paramIndex}\`;
|
|
4670
5040
|
const offsetParam = \`$\${paramIndex + 1}\`;
|
|
4671
|
-
const allParams = [...whereParams, limit, offset];
|
|
5041
|
+
const allParams = [...queryParams, ...whereParams, limit, offset];
|
|
4672
5042
|
|
|
4673
5043
|
// Get total count for pagination
|
|
4674
|
-
const countText = \`SELECT COUNT(*) FROM "\${ctx.table}" \${
|
|
4675
|
-
log.debug(\`LIST \${ctx.table} COUNT SQL:\`, countText, "params:",
|
|
4676
|
-
const countResult = await ctx.pg.query(countText,
|
|
5044
|
+
const countText = \`SELECT COUNT(*) FROM "\${ctx.table}" \${countWhereSQL}\`;
|
|
5045
|
+
log.debug(\`LIST \${ctx.table} COUNT SQL:\`, countText, "params:", countParams);
|
|
5046
|
+
const countResult = await ctx.pg.query(countText, countParams);
|
|
4677
5047
|
const total = parseInt(countResult.rows[0].count, 10);
|
|
4678
5048
|
|
|
4679
5049
|
// Get paginated data
|
|
4680
|
-
const text = \`SELECT
|
|
5050
|
+
const text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
4681
5051
|
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
|
|
4682
5052
|
|
|
4683
5053
|
const { rows } = await ctx.pg.query(text, allParams);
|