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/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 table_name, column_name, is_nullable, udt_name, data_type, column_default
2514
- FROM information_schema.columns
2515
- WHERE table_schema = $1
2516
- ORDER BY table_name, ordinal_position
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
- t.columns.push({
2577
+ const pgType = (r.udt_name ?? r.data_type).toLowerCase();
2578
+ const col = {
2521
2579
  name: r.column_name,
2522
- pgType: (r.udt_name ?? r.data_type).toLowerCase(),
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
- return this.post<Select${Type}>(this.resource, data);
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<Select${Type} | null>(\`\${this.resource}/\${path}\`);
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
- return this.post<PaginatedResponse<Select${Type}>>(\`\${this.resource}/list\`, params ?? {});
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<Select${Type} | null>(\`\${this.resource}/\${path}\`, 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<Select${Type} | null>(\`\${this.resource}/\${path}\`);
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
- * LIST operation - Get multiple records with optional filters
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: { where?: any; limit?: number; offset?: number; include?: any; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[] }
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 (orderBy) {
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}" \${whereSQL}\`;
4675
- log.debug(\`LIST \${ctx.table} COUNT SQL:\`, countText, "params:", whereParams);
4676
- const countResult = await ctx.pg.query(countText, whereParams);
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 * FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
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);