postgresdk 0.15.6 → 0.16.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/README.md CHANGED
@@ -54,6 +54,7 @@ const filtered = await sdk.users.list({
54
54
  - 🔒 **Type Safety** - Full TypeScript types derived from your database schema (including enum types)
55
55
  - ✅ **Runtime Validation** - Zod schemas for request/response validation
56
56
  - 🔗 **Smart Relationships** - Automatic handling of 1:N and M:N relationships with eager loading
57
+ - 🔍 **Vector Search** - Built-in pgvector support for similarity search with multiple distance metrics
57
58
  - 🔐 **Built-in Auth** - API key and JWT authentication
58
59
  - 🎯 **Zero Config** - Works out of the box with sensible defaults
59
60
  - 📦 **Lightweight** - Minimal dependencies, optimized bundle size
@@ -157,7 +158,8 @@ export default {
157
158
  jwt: { // JWT with multi-service support
158
159
  services: [
159
160
  { issuer: "my-app", secret: "env:JWT_SECRET" } // Use "env:" prefix!
160
- ]
161
+ ],
162
+ audience: "my-api" // Optional: validate aud claim
161
163
  }
162
164
  },
163
165
 
@@ -599,6 +601,8 @@ result.limit; // number - page size used
599
601
  result.offset; // number - offset used
600
602
  result.hasMore; // boolean - more pages available
601
603
 
604
+ // Note: Maximum limit is 1000 records per request
605
+
602
606
  // Calculate pagination info
603
607
  const totalPages = Math.ceil(result.total / result.limit);
604
608
  const currentPage = Math.floor(result.offset / result.limit) + 1;
@@ -667,9 +671,127 @@ do {
667
671
  offset += limit;
668
672
  if (!page.hasMore) break;
669
673
  } while (true);
674
+
675
+ // JSONB queries
676
+ const products = await sdk.products.list({
677
+ where: {
678
+ metadata: { $jsonbContains: { tags: ["premium"] } }, // Contains check
679
+ settings: { $jsonbHasKey: "theme" }, // Key exists
680
+ $and: [
681
+ { config: { $jsonbPath: { path: ["price"], operator: "$gte", value: 100 } } }, // Nested value
682
+ { config: { $jsonbPath: { path: ["category"], value: "electronics" } } }
683
+ ]
684
+ }
685
+ });
686
+
687
+ // Type-safe JSONB with generics
688
+ type Metadata = { tags: string[]; stats: { views: number } };
689
+ const users = await sdk.users.list<{ metadata: Metadata }>({
690
+ where: {
691
+ metadata: { $jsonbContains: { tags: ["vip"] } } // Fully typed!
692
+ }
693
+ });
694
+ users.data[0].metadata.stats.views; // TypeScript knows this is a number
695
+ ```
696
+
697
+ #### Vector Search (pgvector)
698
+
699
+ PostgreSDK automatically detects `vector` columns and enables similarity search using [pgvector](https://github.com/pgvector/pgvector). Requires pgvector extension installed.
700
+
701
+ ```sql
702
+ -- Example schema with vector columns
703
+ CREATE EXTENSION vector;
704
+
705
+ CREATE TABLE video_sections (
706
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
707
+ title TEXT NOT NULL,
708
+ vision_embedding vector(1536), -- Image/video embeddings
709
+ text_embedding vector(1536), -- Text embeddings
710
+ created_at TIMESTAMPTZ DEFAULT NOW()
711
+ );
670
712
  ```
671
713
 
672
- See the generated SDK documentation for all available operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$ilike`, `$is`, `$isNot`, `$or`, `$and`.
714
+ ```typescript
715
+ // Basic vector similarity search
716
+ const results = await sdk.video_sections.list({
717
+ vector: {
718
+ field: "vision_embedding",
719
+ query: visionEmbeddingArray, // number[] - your embedding vector
720
+ metric: "cosine" // "cosine" (default), "l2", or "inner"
721
+ },
722
+ limit: 10
723
+ });
724
+
725
+ // Returns records ordered by similarity, with distance included
726
+ results.data.forEach(section => {
727
+ console.log(section.title, section._distance); // _distance auto-included
728
+ });
729
+
730
+ // Distance threshold filtering
731
+ const closeMatches = await sdk.video_sections.list({
732
+ vector: {
733
+ field: "vision_embedding",
734
+ query: embedding,
735
+ metric: "cosine",
736
+ maxDistance: 0.5 // Only return results within this distance
737
+ },
738
+ limit: 50
739
+ });
740
+
741
+ // Hybrid search: combine vector similarity with traditional filters
742
+ const results = await sdk.video_sections.list({
743
+ vector: {
744
+ field: "vision_embedding",
745
+ query: embedding,
746
+ maxDistance: 0.6
747
+ },
748
+ where: {
749
+ status: "published",
750
+ vision_embedding: { $isNot: null } // Ensure embedding exists
751
+ },
752
+ limit: 20
753
+ });
754
+
755
+ // Parallel multi-modal search (vision + text)
756
+ const [visionResults, textResults] = await Promise.all([
757
+ sdk.video_sections.list({
758
+ vector: {
759
+ field: "vision_embedding",
760
+ query: visionQueryEmbedding,
761
+ metric: "cosine",
762
+ maxDistance: 0.6
763
+ },
764
+ where: { vision_embedding: { $isNot: null } },
765
+ limit: 50
766
+ }),
767
+
768
+ sdk.video_sections.list({
769
+ vector: {
770
+ field: "text_embedding",
771
+ query: textQueryEmbedding,
772
+ metric: "cosine",
773
+ maxDistance: 0.5
774
+ },
775
+ where: { text_embedding: { $isNot: null } },
776
+ limit: 50
777
+ })
778
+ ]);
779
+
780
+ // Merge and deduplicate results
781
+ const allResults = [...visionResults.data, ...textResults.data];
782
+ const uniqueResults = Array.from(
783
+ new Map(allResults.map(r => [r.id, r])).values()
784
+ );
785
+ ```
786
+
787
+ **Distance Metrics:**
788
+ - `cosine`: Cosine distance (best for normalized embeddings, range 0-2)
789
+ - `l2`: Euclidean distance (L2 norm)
790
+ - `inner`: Inner product (negative for similarity)
791
+
792
+ **Note:** Vector columns are auto-detected during introspection. Rows with `NULL` embeddings are excluded from vector search results.
793
+
794
+ See the generated SDK documentation for all available operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$ilike`, `$is`, `$isNot`, `$or`, `$and`, `$jsonbContains`, `$jsonbContainedBy`, `$jsonbHasKey`, `$jsonbHasAnyKeys`, `$jsonbHasAllKeys`, `$jsonbPath`.
673
795
 
674
796
  ---
675
797
 
package/dist/cli.js CHANGED
@@ -2510,19 +2510,34 @@ async function introspect(connectionString, schema) {
2510
2510
  for (const r of tablesRows.rows)
2511
2511
  ensureTable(tables, r.table);
2512
2512
  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
2513
+ SELECT
2514
+ c.table_name,
2515
+ c.column_name,
2516
+ c.is_nullable,
2517
+ c.udt_name,
2518
+ c.data_type,
2519
+ c.column_default,
2520
+ a.atttypmod
2521
+ FROM information_schema.columns c
2522
+ LEFT JOIN pg_catalog.pg_class cl ON cl.relname = c.table_name
2523
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cl.relnamespace AND n.nspname = c.table_schema
2524
+ LEFT JOIN pg_catalog.pg_attribute a ON a.attrelid = cl.oid AND a.attname = c.column_name
2525
+ WHERE c.table_schema = $1
2526
+ ORDER BY c.table_name, c.ordinal_position
2517
2527
  `, [schema]);
2518
2528
  for (const r of colsRows.rows) {
2519
2529
  const t = ensureTable(tables, r.table_name);
2520
- t.columns.push({
2530
+ const pgType = (r.udt_name ?? r.data_type).toLowerCase();
2531
+ const col = {
2521
2532
  name: r.column_name,
2522
- pgType: (r.udt_name ?? r.data_type).toLowerCase(),
2533
+ pgType,
2523
2534
  nullable: r.is_nullable === "YES",
2524
2535
  hasDefault: r.column_default != null
2525
- });
2536
+ };
2537
+ if (pgType === "vector" && r.atttypmod != null && r.atttypmod !== -1) {
2538
+ col.vectorDimension = r.atttypmod - 4;
2539
+ }
2540
+ t.columns.push(col);
2526
2541
  }
2527
2542
  const pkRows = await pg.query(`
2528
2543
  SELECT
@@ -2799,6 +2814,7 @@ function emitParamsZod(table, graph) {
2799
2814
  const includeSpecSchema = `z.any()`;
2800
2815
  const pkSchema = hasCompositePk ? `z.object({ ${safePk.map((col) => `${col}: z.string().min(1)`).join(", ")} })` : `z.string().min(1)`;
2801
2816
  return `import { z } from "zod";
2817
+ import { VectorSearchParamsSchema } from "./shared.js";
2802
2818
 
2803
2819
  // Schema for primary key parameters
2804
2820
  export const ${Type}PkSchema = ${pkSchema};
@@ -2809,6 +2825,7 @@ export const ${Type}ListParamsSchema = z.object({
2809
2825
  limit: z.number().int().positive().max(1000).optional(),
2810
2826
  offset: z.number().int().nonnegative().optional(),
2811
2827
  where: z.any().optional(),
2828
+ vector: VectorSearchParamsSchema.optional(),
2812
2829
  orderBy: z.enum([${columnNames}]).optional(),
2813
2830
  order: z.enum(["asc", "desc"]).optional()
2814
2831
  }).strict();
@@ -2835,7 +2852,16 @@ export const PaginationParamsSchema = z.object({
2835
2852
  offset: z.number().int().nonnegative().optional()
2836
2853
  }).strict();
2837
2854
 
2855
+ // Shared vector search schema (used across all tables)
2856
+ export const VectorSearchParamsSchema = z.object({
2857
+ field: z.string().min(1),
2858
+ query: z.array(z.number()),
2859
+ metric: z.enum(["cosine", "l2", "inner"]).optional(),
2860
+ maxDistance: z.number().nonnegative().optional()
2861
+ }).strict();
2862
+
2838
2863
  export type PaginationParams = z.infer<typeof PaginationParamsSchema>;
2864
+ export type VectorSearchParams = z.infer<typeof VectorSearchParamsSchema>;
2839
2865
  `;
2840
2866
  }
2841
2867
 
@@ -2904,7 +2930,13 @@ const listSchema = z.object({
2904
2930
  limit: z.number().int().positive().max(1000).optional(),
2905
2931
  offset: z.number().int().min(0).optional(),
2906
2932
  orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
2907
- order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional()
2933
+ order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional(),
2934
+ vector: z.object({
2935
+ field: z.string(),
2936
+ query: z.array(z.number()),
2937
+ metric: z.enum(["cosine", "l2", "inner"]).optional(),
2938
+ maxDistance: z.number().optional()
2939
+ }).optional()
2908
2940
  });
2909
2941
 
2910
2942
  /**
@@ -3168,6 +3200,13 @@ ${typeImports}
3168
3200
  ${otherTableImports.join(`
3169
3201
  `)}
3170
3202
 
3203
+ /**
3204
+ * Helper type to merge JSONB type overrides into base types
3205
+ * @example
3206
+ * type UserWithMetadata = MergeJsonb<SelectUser, { metadata: { tags: string[] } }>;
3207
+ */
3208
+ type MergeJsonb<TBase, TJsonb> = Omit<TBase, keyof TJsonb> & TJsonb;
3209
+
3171
3210
  /**
3172
3211
  * Client for ${table.name} table operations
3173
3212
  */
@@ -3179,8 +3218,20 @@ export class ${Type}Client extends BaseClient {
3179
3218
  * @param data - The data to insert
3180
3219
  * @returns The created record
3181
3220
  */
3182
- async create(data: Insert${Type}): Promise<Select${Type}> {
3183
- return this.post<Select${Type}>(this.resource, data);
3221
+ async create(data: Insert${Type}): Promise<Select${Type}>;
3222
+ /**
3223
+ * Create a new ${table.name} record with JSONB type overrides
3224
+ * @param data - The data to insert
3225
+ * @returns The created record with typed JSONB fields
3226
+ * @example
3227
+ * type Metadata = { tags: string[]; prefs: { theme: 'light' | 'dark' } };
3228
+ * const user = await client.create<{ metadata: Metadata }>({ name: 'Alice', metadata: { tags: [], prefs: { theme: 'light' } } });
3229
+ */
3230
+ async create<TJsonb extends Partial<Select${Type}>>(
3231
+ data: MergeJsonb<Insert${Type}, TJsonb>
3232
+ ): Promise<MergeJsonb<Select${Type}, TJsonb>>;
3233
+ async create(data: any): Promise<any> {
3234
+ return this.post<any>(this.resource, data);
3184
3235
  }
3185
3236
 
3186
3237
  /**
@@ -3188,9 +3239,18 @@ export class ${Type}Client extends BaseClient {
3188
3239
  * @param pk - The primary key value${hasCompositePk ? "s" : ""}
3189
3240
  * @returns The record if found, null otherwise
3190
3241
  */
3191
- async getByPk(pk: ${pkType}): Promise<Select${Type} | null> {
3242
+ async getByPk(pk: ${pkType}): Promise<Select${Type} | null>;
3243
+ /**
3244
+ * Get a ${table.name} record by primary key with JSONB type overrides
3245
+ * @param pk - The primary key value${hasCompositePk ? "s" : ""}
3246
+ * @returns The record with typed JSONB fields if found, null otherwise
3247
+ */
3248
+ async getByPk<TJsonb extends Partial<Select${Type}>>(
3249
+ pk: ${pkType}
3250
+ ): Promise<MergeJsonb<Select${Type}, TJsonb> | null>;
3251
+ async getByPk(pk: ${pkType}): Promise<any> {
3192
3252
  const path = ${pkPathExpr};
3193
- return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
3253
+ return this.get<any>(\`\${this.resource}/\${path}\`);
3194
3254
  }
3195
3255
 
3196
3256
  /**
@@ -3211,8 +3271,48 @@ export class ${Type}Client extends BaseClient {
3211
3271
  where?: Where<Select${Type}>;
3212
3272
  orderBy?: string | string[];
3213
3273
  order?: "asc" | "desc" | ("asc" | "desc")[];
3214
- }): Promise<PaginatedResponse<Select${Type}>> {
3215
- return this.post<PaginatedResponse<Select${Type}>>(\`\${this.resource}/list\`, params ?? {});
3274
+ }): Promise<PaginatedResponse<Select${Type}>>;
3275
+ /**
3276
+ * List ${table.name} records with vector similarity search
3277
+ * @param params - Query parameters with vector search enabled
3278
+ * @param params.vector - Vector similarity search configuration
3279
+ * @returns Paginated results with _distance field included
3280
+ */
3281
+ async list(params: {
3282
+ include?: any;
3283
+ limit?: number;
3284
+ offset?: number;
3285
+ where?: Where<Select${Type}>;
3286
+ vector: {
3287
+ field: string;
3288
+ query: number[];
3289
+ metric?: "cosine" | "l2" | "inner";
3290
+ maxDistance?: number;
3291
+ };
3292
+ orderBy?: string | string[];
3293
+ order?: "asc" | "desc" | ("asc" | "desc")[];
3294
+ }): Promise<PaginatedResponse<Select${Type} & { _distance: number }>>;
3295
+ /**
3296
+ * List ${table.name} records with pagination and filtering, with JSONB type overrides
3297
+ * @param params - Query parameters with typed JSONB fields in where clause
3298
+ * @returns Paginated results with typed JSONB fields
3299
+ */
3300
+ async list<TJsonb extends Partial<Select${Type}>>(params?: {
3301
+ include?: any;
3302
+ limit?: number;
3303
+ offset?: number;
3304
+ where?: Where<MergeJsonb<Select${Type}, TJsonb>>;
3305
+ vector?: {
3306
+ field: string;
3307
+ query: number[];
3308
+ metric?: "cosine" | "l2" | "inner";
3309
+ maxDistance?: number;
3310
+ };
3311
+ orderBy?: string | string[];
3312
+ order?: "asc" | "desc" | ("asc" | "desc")[];
3313
+ }): Promise<PaginatedResponse<MergeJsonb<Select${Type}, TJsonb>>>;
3314
+ async list(params?: any): Promise<any> {
3315
+ return this.post<any>(\`\${this.resource}/list\`, params ?? {});
3216
3316
  }
3217
3317
 
3218
3318
  /**
@@ -3221,9 +3321,20 @@ export class ${Type}Client extends BaseClient {
3221
3321
  * @param patch - Partial data to update
3222
3322
  * @returns The updated record if found, null otherwise
3223
3323
  */
3224
- async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null> {
3324
+ async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null>;
3325
+ /**
3326
+ * Update a ${table.name} record by primary key with JSONB type overrides
3327
+ * @param pk - The primary key value${hasCompositePk ? "s" : ""}
3328
+ * @param patch - Partial data to update with typed JSONB fields
3329
+ * @returns The updated record with typed JSONB fields if found, null otherwise
3330
+ */
3331
+ async update<TJsonb extends Partial<Select${Type}>>(
3332
+ pk: ${pkType},
3333
+ patch: MergeJsonb<Update${Type}, TJsonb>
3334
+ ): Promise<MergeJsonb<Select${Type}, TJsonb> | null>;
3335
+ async update(pk: ${pkType}, patch: any): Promise<any> {
3225
3336
  const path = ${pkPathExpr};
3226
- return this.patch<Select${Type} | null>(\`\${this.resource}/\${path}\`, patch);
3337
+ return this.patch<any>(\`\${this.resource}/\${path}\`, patch);
3227
3338
  }
3228
3339
 
3229
3340
  /**
@@ -3231,9 +3342,18 @@ export class ${Type}Client extends BaseClient {
3231
3342
  * @param pk - The primary key value${hasCompositePk ? "s" : ""}
3232
3343
  * @returns The deleted record if found, null otherwise
3233
3344
  */
3234
- async delete(pk: ${pkType}): Promise<Select${Type} | null> {
3345
+ async delete(pk: ${pkType}): Promise<Select${Type} | null>;
3346
+ /**
3347
+ * Delete a ${table.name} record by primary key with JSONB type overrides
3348
+ * @param pk - The primary key value${hasCompositePk ? "s" : ""}
3349
+ * @returns The deleted record with typed JSONB fields if found, null otherwise
3350
+ */
3351
+ async delete<TJsonb extends Partial<Select${Type}>>(
3352
+ pk: ${pkType}
3353
+ ): Promise<MergeJsonb<Select${Type}, TJsonb> | null>;
3354
+ async delete(pk: ${pkType}): Promise<any> {
3235
3355
  const path = ${pkPathExpr};
3236
- return this.del<Select${Type} | null>(\`\${this.resource}/\${path}\`);
3356
+ return this.del<any>(\`\${this.resource}/\${path}\`);
3237
3357
  }
3238
3358
  ${includeMethodsCode}}
3239
3359
  `;
@@ -3904,6 +4024,25 @@ function emitWhereTypes() {
3904
4024
  * To make changes, modify your schema or configuration and regenerate.
3905
4025
  */
3906
4026
 
4027
+ /**
4028
+ * Deep partial type for JSONB contains operator
4029
+ */
4030
+ type DeepPartial<T> = T extends object ? {
4031
+ [P in keyof T]?: DeepPartial<T[P]>;
4032
+ } : T;
4033
+
4034
+ /**
4035
+ * JSONB path query configuration
4036
+ */
4037
+ type JsonbPathQuery = {
4038
+ /** Array of keys to traverse (e.g., ['user', 'preferences', 'theme']) */
4039
+ path: string[];
4040
+ /** Operator to apply to the value at the path (defaults to '$eq') */
4041
+ operator?: '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$like' | '$ilike';
4042
+ /** Value to compare against */
4043
+ value: any;
4044
+ };
4045
+
3907
4046
  /**
3908
4047
  * WHERE clause operators for filtering
3909
4048
  */
@@ -3932,6 +4071,20 @@ export type WhereOperator<T> = {
3932
4071
  $is?: null;
3933
4072
  /** IS NOT NULL */
3934
4073
  $isNot?: null;
4074
+
4075
+ // JSONB operators (only available for object/unknown types)
4076
+ /** JSONB contains (@>) - check if column contains the specified JSON structure */
4077
+ $jsonbContains?: T extends object | unknown ? (unknown extends T ? any : DeepPartial<T>) : never;
4078
+ /** JSONB contained by (<@) - check if column is contained by the specified JSON */
4079
+ $jsonbContainedBy?: T extends object | unknown ? any : never;
4080
+ /** JSONB has key (?) - check if top-level key exists */
4081
+ $jsonbHasKey?: T extends object | unknown ? string : never;
4082
+ /** JSONB has any keys (?|) - check if any of the specified keys exist */
4083
+ $jsonbHasAnyKeys?: T extends object | unknown ? string[] : never;
4084
+ /** JSONB has all keys (?&) - check if all of the specified keys exist */
4085
+ $jsonbHasAllKeys?: T extends object | unknown ? string[] : never;
4086
+ /** JSONB path query - query nested values. For multiple paths on same column, use $and */
4087
+ $jsonbPath?: T extends object | unknown ? JsonbPathQuery : never;
3935
4088
  };
3936
4089
 
3937
4090
  /**
@@ -4565,6 +4718,100 @@ function buildWhereClause(
4565
4718
  whereParts.push(\`"\${key}" IS NOT NULL\`);
4566
4719
  }
4567
4720
  break;
4721
+ case '$jsonbContains':
4722
+ whereParts.push(\`"\${key}" @> $\${paramIndex}\`);
4723
+ whereParams.push(JSON.stringify(opValue));
4724
+ paramIndex++;
4725
+ break;
4726
+ case '$jsonbContainedBy':
4727
+ whereParts.push(\`"\${key}" <@ $\${paramIndex}\`);
4728
+ whereParams.push(JSON.stringify(opValue));
4729
+ paramIndex++;
4730
+ break;
4731
+ case '$jsonbHasKey':
4732
+ whereParts.push(\`"\${key}" ? $\${paramIndex}\`);
4733
+ whereParams.push(opValue);
4734
+ paramIndex++;
4735
+ break;
4736
+ case '$jsonbHasAnyKeys':
4737
+ if (Array.isArray(opValue) && opValue.length > 0) {
4738
+ whereParts.push(\`"\${key}" ?| $\${paramIndex}\`);
4739
+ whereParams.push(opValue);
4740
+ paramIndex++;
4741
+ }
4742
+ break;
4743
+ case '$jsonbHasAllKeys':
4744
+ if (Array.isArray(opValue) && opValue.length > 0) {
4745
+ whereParts.push(\`"\${key}" ?& $\${paramIndex}\`);
4746
+ whereParams.push(opValue);
4747
+ paramIndex++;
4748
+ }
4749
+ break;
4750
+ case '$jsonbPath':
4751
+ const pathConfig = opValue;
4752
+ const pathKeys = pathConfig.path;
4753
+ const pathOperator = pathConfig.operator || '$eq';
4754
+ const pathValue = pathConfig.value;
4755
+
4756
+ if (!Array.isArray(pathKeys) || pathKeys.length === 0) {
4757
+ break;
4758
+ }
4759
+
4760
+ // Build path accessor: metadata->'user'->'preferences'->>'theme'
4761
+ // Use -> for all keys except the last one, use ->> for the last to get text
4762
+ const pathParts = pathKeys.slice(0, -1);
4763
+ const lastKey = pathKeys[pathKeys.length - 1];
4764
+
4765
+ let pathExpr = \`"\${key}"\`;
4766
+ for (const part of pathParts) {
4767
+ pathExpr += \`->'\${part}'\`;
4768
+ }
4769
+ pathExpr += \`->>'\${lastKey}'\`;
4770
+
4771
+ // Apply the operator
4772
+ switch (pathOperator) {
4773
+ case '$eq':
4774
+ whereParts.push(\`\${pathExpr} = $\${paramIndex}\`);
4775
+ whereParams.push(String(pathValue));
4776
+ paramIndex++;
4777
+ break;
4778
+ case '$ne':
4779
+ whereParts.push(\`\${pathExpr} != $\${paramIndex}\`);
4780
+ whereParams.push(String(pathValue));
4781
+ paramIndex++;
4782
+ break;
4783
+ case '$gt':
4784
+ whereParts.push(\`(\${pathExpr})::numeric > $\${paramIndex}\`);
4785
+ whereParams.push(pathValue);
4786
+ paramIndex++;
4787
+ break;
4788
+ case '$gte':
4789
+ whereParts.push(\`(\${pathExpr})::numeric >= $\${paramIndex}\`);
4790
+ whereParams.push(pathValue);
4791
+ paramIndex++;
4792
+ break;
4793
+ case '$lt':
4794
+ whereParts.push(\`(\${pathExpr})::numeric < $\${paramIndex}\`);
4795
+ whereParams.push(pathValue);
4796
+ paramIndex++;
4797
+ break;
4798
+ case '$lte':
4799
+ whereParts.push(\`(\${pathExpr})::numeric <= $\${paramIndex}\`);
4800
+ whereParams.push(pathValue);
4801
+ paramIndex++;
4802
+ break;
4803
+ case '$like':
4804
+ whereParts.push(\`\${pathExpr} LIKE $\${paramIndex}\`);
4805
+ whereParams.push(pathValue);
4806
+ paramIndex++;
4807
+ break;
4808
+ case '$ilike':
4809
+ whereParts.push(\`\${pathExpr} ILIKE $\${paramIndex}\`);
4810
+ whereParams.push(pathValue);
4811
+ paramIndex++;
4812
+ break;
4813
+ }
4814
+ break;
4568
4815
  }
4569
4816
  }
4570
4817
  } else if (value === null) {
@@ -4620,17 +4867,51 @@ function buildWhereClause(
4620
4867
  }
4621
4868
 
4622
4869
  /**
4623
- * LIST operation - Get multiple records with optional filters
4870
+ * Get distance operator for vector similarity search
4871
+ */
4872
+ function getVectorDistanceOperator(metric?: string): string {
4873
+ switch (metric) {
4874
+ case "l2":
4875
+ return "<->";
4876
+ case "inner":
4877
+ return "<#>";
4878
+ case "cosine":
4879
+ default:
4880
+ return "<=>";
4881
+ }
4882
+ }
4883
+
4884
+ /**
4885
+ * LIST operation - Get multiple records with optional filters and vector search
4624
4886
  */
4625
4887
  export async function listRecords(
4626
4888
  ctx: OperationContext,
4627
- params: { where?: any; limit?: number; offset?: number; include?: any; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[] }
4889
+ params: {
4890
+ where?: any;
4891
+ limit?: number;
4892
+ offset?: number;
4893
+ include?: any;
4894
+ orderBy?: string | string[];
4895
+ order?: "asc" | "desc" | ("asc" | "desc")[];
4896
+ vector?: {
4897
+ field: string;
4898
+ query: number[];
4899
+ metric?: "cosine" | "l2" | "inner";
4900
+ maxDistance?: number;
4901
+ };
4902
+ }
4628
4903
  ): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
4629
4904
  try {
4630
- const { where: whereClause, limit = 50, offset = 0, include, orderBy, order } = params;
4905
+ const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector } = params;
4906
+
4907
+ // Get distance operator if vector search
4908
+ const distanceOp = vector ? getVectorDistanceOperator(vector.metric) : "";
4631
4909
 
4632
- // Build WHERE clause
4633
- let paramIndex = 1;
4910
+ // Add vector to params array if present
4911
+ const queryParams: any[] = vector ? [JSON.stringify(vector.query)] : [];
4912
+
4913
+ // Build WHERE clause for SELECT/UPDATE queries (with vector as $1 if present)
4914
+ let paramIndex = vector ? 2 : 1;
4634
4915
  const whereParts: string[] = [];
4635
4916
  let whereParams: any[] = [];
4636
4917
 
@@ -4649,11 +4930,47 @@ export async function listRecords(
4649
4930
  }
4650
4931
  }
4651
4932
 
4933
+ // Add vector distance threshold filter if specified
4934
+ if (vector?.maxDistance !== undefined) {
4935
+ whereParts.push(\`("\${vector.field}" \${distanceOp} ($1)::vector) < \${vector.maxDistance}\`);
4936
+ }
4937
+
4652
4938
  const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
4653
4939
 
4940
+ // Build WHERE clause for COUNT query (may need different param indices)
4941
+ let countWhereSQL = whereSQL;
4942
+ let countParams = whereParams;
4943
+
4944
+ if (vector && vector.maxDistance === undefined && whereParams.length > 0) {
4945
+ // COUNT query doesn't use vector, so rebuild WHERE without vector offset
4946
+ const countWhereParts: string[] = [];
4947
+ if (ctx.softDeleteColumn) {
4948
+ countWhereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
4949
+ }
4950
+ if (whereClause) {
4951
+ const result = buildWhereClause(whereClause, 1); // Start at $1 for count
4952
+ if (result.sql) {
4953
+ countWhereParts.push(result.sql);
4954
+ countParams = result.params;
4955
+ }
4956
+ }
4957
+ countWhereSQL = countWhereParts.length > 0 ? \`WHERE \${countWhereParts.join(" AND ")}\` : "";
4958
+ } else if (vector?.maxDistance !== undefined) {
4959
+ // COUNT query includes vector for maxDistance filter
4960
+ countParams = [...queryParams, ...whereParams];
4961
+ }
4962
+
4963
+ // Build SELECT clause
4964
+ const selectClause = vector
4965
+ ? \`*, ("\${vector.field}" \${distanceOp} ($1)::vector) AS _distance\`
4966
+ : "*";
4967
+
4654
4968
  // Build ORDER BY clause
4655
4969
  let orderBySQL = "";
4656
- if (orderBy) {
4970
+ if (vector) {
4971
+ // For vector search, always order by distance
4972
+ orderBySQL = \`ORDER BY "\${vector.field}" \${distanceOp} ($1)::vector\`;
4973
+ } else if (orderBy) {
4657
4974
  const columns = Array.isArray(orderBy) ? orderBy : [orderBy];
4658
4975
  const directions = Array.isArray(order) ? order : (order ? Array(columns.length).fill(order) : Array(columns.length).fill("asc"));
4659
4976
 
@@ -4668,16 +4985,16 @@ export async function listRecords(
4668
4985
  // Add limit and offset params
4669
4986
  const limitParam = \`$\${paramIndex}\`;
4670
4987
  const offsetParam = \`$\${paramIndex + 1}\`;
4671
- const allParams = [...whereParams, limit, offset];
4988
+ const allParams = [...queryParams, ...whereParams, limit, offset];
4672
4989
 
4673
4990
  // 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);
4991
+ const countText = \`SELECT COUNT(*) FROM "\${ctx.table}" \${countWhereSQL}\`;
4992
+ log.debug(\`LIST \${ctx.table} COUNT SQL:\`, countText, "params:", countParams);
4993
+ const countResult = await ctx.pg.query(countText, countParams);
4677
4994
  const total = parseInt(countResult.rows[0].count, 10);
4678
4995
 
4679
4996
  // Get paginated data
4680
- const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
4997
+ const text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
4681
4998
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
4682
4999
 
4683
5000
  const { rows } = await ctx.pg.query(text, allParams);
package/dist/index.js CHANGED
@@ -1681,19 +1681,34 @@ async function introspect(connectionString, schema) {
1681
1681
  for (const r of tablesRows.rows)
1682
1682
  ensureTable(tables, r.table);
1683
1683
  const colsRows = await pg.query(`
1684
- SELECT table_name, column_name, is_nullable, udt_name, data_type, column_default
1685
- FROM information_schema.columns
1686
- WHERE table_schema = $1
1687
- ORDER BY table_name, ordinal_position
1684
+ SELECT
1685
+ c.table_name,
1686
+ c.column_name,
1687
+ c.is_nullable,
1688
+ c.udt_name,
1689
+ c.data_type,
1690
+ c.column_default,
1691
+ a.atttypmod
1692
+ FROM information_schema.columns c
1693
+ LEFT JOIN pg_catalog.pg_class cl ON cl.relname = c.table_name
1694
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cl.relnamespace AND n.nspname = c.table_schema
1695
+ LEFT JOIN pg_catalog.pg_attribute a ON a.attrelid = cl.oid AND a.attname = c.column_name
1696
+ WHERE c.table_schema = $1
1697
+ ORDER BY c.table_name, c.ordinal_position
1688
1698
  `, [schema]);
1689
1699
  for (const r of colsRows.rows) {
1690
1700
  const t = ensureTable(tables, r.table_name);
1691
- t.columns.push({
1701
+ const pgType = (r.udt_name ?? r.data_type).toLowerCase();
1702
+ const col = {
1692
1703
  name: r.column_name,
1693
- pgType: (r.udt_name ?? r.data_type).toLowerCase(),
1704
+ pgType,
1694
1705
  nullable: r.is_nullable === "YES",
1695
1706
  hasDefault: r.column_default != null
1696
- });
1707
+ };
1708
+ if (pgType === "vector" && r.atttypmod != null && r.atttypmod !== -1) {
1709
+ col.vectorDimension = r.atttypmod - 4;
1710
+ }
1711
+ t.columns.push(col);
1697
1712
  }
1698
1713
  const pkRows = await pg.query(`
1699
1714
  SELECT
@@ -1970,6 +1985,7 @@ function emitParamsZod(table, graph) {
1970
1985
  const includeSpecSchema = `z.any()`;
1971
1986
  const pkSchema = hasCompositePk ? `z.object({ ${safePk.map((col) => `${col}: z.string().min(1)`).join(", ")} })` : `z.string().min(1)`;
1972
1987
  return `import { z } from "zod";
1988
+ import { VectorSearchParamsSchema } from "./shared.js";
1973
1989
 
1974
1990
  // Schema for primary key parameters
1975
1991
  export const ${Type}PkSchema = ${pkSchema};
@@ -1980,6 +1996,7 @@ export const ${Type}ListParamsSchema = z.object({
1980
1996
  limit: z.number().int().positive().max(1000).optional(),
1981
1997
  offset: z.number().int().nonnegative().optional(),
1982
1998
  where: z.any().optional(),
1999
+ vector: VectorSearchParamsSchema.optional(),
1983
2000
  orderBy: z.enum([${columnNames}]).optional(),
1984
2001
  order: z.enum(["asc", "desc"]).optional()
1985
2002
  }).strict();
@@ -2006,7 +2023,16 @@ export const PaginationParamsSchema = z.object({
2006
2023
  offset: z.number().int().nonnegative().optional()
2007
2024
  }).strict();
2008
2025
 
2026
+ // Shared vector search schema (used across all tables)
2027
+ export const VectorSearchParamsSchema = z.object({
2028
+ field: z.string().min(1),
2029
+ query: z.array(z.number()),
2030
+ metric: z.enum(["cosine", "l2", "inner"]).optional(),
2031
+ maxDistance: z.number().nonnegative().optional()
2032
+ }).strict();
2033
+
2009
2034
  export type PaginationParams = z.infer<typeof PaginationParamsSchema>;
2035
+ export type VectorSearchParams = z.infer<typeof VectorSearchParamsSchema>;
2010
2036
  `;
2011
2037
  }
2012
2038
 
@@ -2075,7 +2101,13 @@ const listSchema = z.object({
2075
2101
  limit: z.number().int().positive().max(1000).optional(),
2076
2102
  offset: z.number().int().min(0).optional(),
2077
2103
  orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
2078
- order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional()
2104
+ order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional(),
2105
+ vector: z.object({
2106
+ field: z.string(),
2107
+ query: z.array(z.number()),
2108
+ metric: z.enum(["cosine", "l2", "inner"]).optional(),
2109
+ maxDistance: z.number().optional()
2110
+ }).optional()
2079
2111
  });
2080
2112
 
2081
2113
  /**
@@ -2339,6 +2371,13 @@ ${typeImports}
2339
2371
  ${otherTableImports.join(`
2340
2372
  `)}
2341
2373
 
2374
+ /**
2375
+ * Helper type to merge JSONB type overrides into base types
2376
+ * @example
2377
+ * type UserWithMetadata = MergeJsonb<SelectUser, { metadata: { tags: string[] } }>;
2378
+ */
2379
+ type MergeJsonb<TBase, TJsonb> = Omit<TBase, keyof TJsonb> & TJsonb;
2380
+
2342
2381
  /**
2343
2382
  * Client for ${table.name} table operations
2344
2383
  */
@@ -2350,8 +2389,20 @@ export class ${Type}Client extends BaseClient {
2350
2389
  * @param data - The data to insert
2351
2390
  * @returns The created record
2352
2391
  */
2353
- async create(data: Insert${Type}): Promise<Select${Type}> {
2354
- return this.post<Select${Type}>(this.resource, data);
2392
+ async create(data: Insert${Type}): Promise<Select${Type}>;
2393
+ /**
2394
+ * Create a new ${table.name} record with JSONB type overrides
2395
+ * @param data - The data to insert
2396
+ * @returns The created record with typed JSONB fields
2397
+ * @example
2398
+ * type Metadata = { tags: string[]; prefs: { theme: 'light' | 'dark' } };
2399
+ * const user = await client.create<{ metadata: Metadata }>({ name: 'Alice', metadata: { tags: [], prefs: { theme: 'light' } } });
2400
+ */
2401
+ async create<TJsonb extends Partial<Select${Type}>>(
2402
+ data: MergeJsonb<Insert${Type}, TJsonb>
2403
+ ): Promise<MergeJsonb<Select${Type}, TJsonb>>;
2404
+ async create(data: any): Promise<any> {
2405
+ return this.post<any>(this.resource, data);
2355
2406
  }
2356
2407
 
2357
2408
  /**
@@ -2359,9 +2410,18 @@ export class ${Type}Client extends BaseClient {
2359
2410
  * @param pk - The primary key value${hasCompositePk ? "s" : ""}
2360
2411
  * @returns The record if found, null otherwise
2361
2412
  */
2362
- async getByPk(pk: ${pkType}): Promise<Select${Type} | null> {
2413
+ async getByPk(pk: ${pkType}): Promise<Select${Type} | null>;
2414
+ /**
2415
+ * Get a ${table.name} record by primary key with JSONB type overrides
2416
+ * @param pk - The primary key value${hasCompositePk ? "s" : ""}
2417
+ * @returns The record with typed JSONB fields if found, null otherwise
2418
+ */
2419
+ async getByPk<TJsonb extends Partial<Select${Type}>>(
2420
+ pk: ${pkType}
2421
+ ): Promise<MergeJsonb<Select${Type}, TJsonb> | null>;
2422
+ async getByPk(pk: ${pkType}): Promise<any> {
2363
2423
  const path = ${pkPathExpr};
2364
- return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
2424
+ return this.get<any>(\`\${this.resource}/\${path}\`);
2365
2425
  }
2366
2426
 
2367
2427
  /**
@@ -2382,8 +2442,48 @@ export class ${Type}Client extends BaseClient {
2382
2442
  where?: Where<Select${Type}>;
2383
2443
  orderBy?: string | string[];
2384
2444
  order?: "asc" | "desc" | ("asc" | "desc")[];
2385
- }): Promise<PaginatedResponse<Select${Type}>> {
2386
- return this.post<PaginatedResponse<Select${Type}>>(\`\${this.resource}/list\`, params ?? {});
2445
+ }): Promise<PaginatedResponse<Select${Type}>>;
2446
+ /**
2447
+ * List ${table.name} records with vector similarity search
2448
+ * @param params - Query parameters with vector search enabled
2449
+ * @param params.vector - Vector similarity search configuration
2450
+ * @returns Paginated results with _distance field included
2451
+ */
2452
+ async list(params: {
2453
+ include?: any;
2454
+ limit?: number;
2455
+ offset?: number;
2456
+ where?: Where<Select${Type}>;
2457
+ vector: {
2458
+ field: string;
2459
+ query: number[];
2460
+ metric?: "cosine" | "l2" | "inner";
2461
+ maxDistance?: number;
2462
+ };
2463
+ orderBy?: string | string[];
2464
+ order?: "asc" | "desc" | ("asc" | "desc")[];
2465
+ }): Promise<PaginatedResponse<Select${Type} & { _distance: number }>>;
2466
+ /**
2467
+ * List ${table.name} records with pagination and filtering, with JSONB type overrides
2468
+ * @param params - Query parameters with typed JSONB fields in where clause
2469
+ * @returns Paginated results with typed JSONB fields
2470
+ */
2471
+ async list<TJsonb extends Partial<Select${Type}>>(params?: {
2472
+ include?: any;
2473
+ limit?: number;
2474
+ offset?: number;
2475
+ where?: Where<MergeJsonb<Select${Type}, TJsonb>>;
2476
+ vector?: {
2477
+ field: string;
2478
+ query: number[];
2479
+ metric?: "cosine" | "l2" | "inner";
2480
+ maxDistance?: number;
2481
+ };
2482
+ orderBy?: string | string[];
2483
+ order?: "asc" | "desc" | ("asc" | "desc")[];
2484
+ }): Promise<PaginatedResponse<MergeJsonb<Select${Type}, TJsonb>>>;
2485
+ async list(params?: any): Promise<any> {
2486
+ return this.post<any>(\`\${this.resource}/list\`, params ?? {});
2387
2487
  }
2388
2488
 
2389
2489
  /**
@@ -2392,9 +2492,20 @@ export class ${Type}Client extends BaseClient {
2392
2492
  * @param patch - Partial data to update
2393
2493
  * @returns The updated record if found, null otherwise
2394
2494
  */
2395
- async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null> {
2495
+ async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null>;
2496
+ /**
2497
+ * Update a ${table.name} record by primary key with JSONB type overrides
2498
+ * @param pk - The primary key value${hasCompositePk ? "s" : ""}
2499
+ * @param patch - Partial data to update with typed JSONB fields
2500
+ * @returns The updated record with typed JSONB fields if found, null otherwise
2501
+ */
2502
+ async update<TJsonb extends Partial<Select${Type}>>(
2503
+ pk: ${pkType},
2504
+ patch: MergeJsonb<Update${Type}, TJsonb>
2505
+ ): Promise<MergeJsonb<Select${Type}, TJsonb> | null>;
2506
+ async update(pk: ${pkType}, patch: any): Promise<any> {
2396
2507
  const path = ${pkPathExpr};
2397
- return this.patch<Select${Type} | null>(\`\${this.resource}/\${path}\`, patch);
2508
+ return this.patch<any>(\`\${this.resource}/\${path}\`, patch);
2398
2509
  }
2399
2510
 
2400
2511
  /**
@@ -2402,9 +2513,18 @@ export class ${Type}Client extends BaseClient {
2402
2513
  * @param pk - The primary key value${hasCompositePk ? "s" : ""}
2403
2514
  * @returns The deleted record if found, null otherwise
2404
2515
  */
2405
- async delete(pk: ${pkType}): Promise<Select${Type} | null> {
2516
+ async delete(pk: ${pkType}): Promise<Select${Type} | null>;
2517
+ /**
2518
+ * Delete a ${table.name} record by primary key with JSONB type overrides
2519
+ * @param pk - The primary key value${hasCompositePk ? "s" : ""}
2520
+ * @returns The deleted record with typed JSONB fields if found, null otherwise
2521
+ */
2522
+ async delete<TJsonb extends Partial<Select${Type}>>(
2523
+ pk: ${pkType}
2524
+ ): Promise<MergeJsonb<Select${Type}, TJsonb> | null>;
2525
+ async delete(pk: ${pkType}): Promise<any> {
2406
2526
  const path = ${pkPathExpr};
2407
- return this.del<Select${Type} | null>(\`\${this.resource}/\${path}\`);
2527
+ return this.del<any>(\`\${this.resource}/\${path}\`);
2408
2528
  }
2409
2529
  ${includeMethodsCode}}
2410
2530
  `;
@@ -3075,6 +3195,25 @@ function emitWhereTypes() {
3075
3195
  * To make changes, modify your schema or configuration and regenerate.
3076
3196
  */
3077
3197
 
3198
+ /**
3199
+ * Deep partial type for JSONB contains operator
3200
+ */
3201
+ type DeepPartial<T> = T extends object ? {
3202
+ [P in keyof T]?: DeepPartial<T[P]>;
3203
+ } : T;
3204
+
3205
+ /**
3206
+ * JSONB path query configuration
3207
+ */
3208
+ type JsonbPathQuery = {
3209
+ /** Array of keys to traverse (e.g., ['user', 'preferences', 'theme']) */
3210
+ path: string[];
3211
+ /** Operator to apply to the value at the path (defaults to '$eq') */
3212
+ operator?: '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$like' | '$ilike';
3213
+ /** Value to compare against */
3214
+ value: any;
3215
+ };
3216
+
3078
3217
  /**
3079
3218
  * WHERE clause operators for filtering
3080
3219
  */
@@ -3103,6 +3242,20 @@ export type WhereOperator<T> = {
3103
3242
  $is?: null;
3104
3243
  /** IS NOT NULL */
3105
3244
  $isNot?: null;
3245
+
3246
+ // JSONB operators (only available for object/unknown types)
3247
+ /** JSONB contains (@>) - check if column contains the specified JSON structure */
3248
+ $jsonbContains?: T extends object | unknown ? (unknown extends T ? any : DeepPartial<T>) : never;
3249
+ /** JSONB contained by (<@) - check if column is contained by the specified JSON */
3250
+ $jsonbContainedBy?: T extends object | unknown ? any : never;
3251
+ /** JSONB has key (?) - check if top-level key exists */
3252
+ $jsonbHasKey?: T extends object | unknown ? string : never;
3253
+ /** JSONB has any keys (?|) - check if any of the specified keys exist */
3254
+ $jsonbHasAnyKeys?: T extends object | unknown ? string[] : never;
3255
+ /** JSONB has all keys (?&) - check if all of the specified keys exist */
3256
+ $jsonbHasAllKeys?: T extends object | unknown ? string[] : never;
3257
+ /** JSONB path query - query nested values. For multiple paths on same column, use $and */
3258
+ $jsonbPath?: T extends object | unknown ? JsonbPathQuery : never;
3106
3259
  };
3107
3260
 
3108
3261
  /**
@@ -3736,6 +3889,100 @@ function buildWhereClause(
3736
3889
  whereParts.push(\`"\${key}" IS NOT NULL\`);
3737
3890
  }
3738
3891
  break;
3892
+ case '$jsonbContains':
3893
+ whereParts.push(\`"\${key}" @> $\${paramIndex}\`);
3894
+ whereParams.push(JSON.stringify(opValue));
3895
+ paramIndex++;
3896
+ break;
3897
+ case '$jsonbContainedBy':
3898
+ whereParts.push(\`"\${key}" <@ $\${paramIndex}\`);
3899
+ whereParams.push(JSON.stringify(opValue));
3900
+ paramIndex++;
3901
+ break;
3902
+ case '$jsonbHasKey':
3903
+ whereParts.push(\`"\${key}" ? $\${paramIndex}\`);
3904
+ whereParams.push(opValue);
3905
+ paramIndex++;
3906
+ break;
3907
+ case '$jsonbHasAnyKeys':
3908
+ if (Array.isArray(opValue) && opValue.length > 0) {
3909
+ whereParts.push(\`"\${key}" ?| $\${paramIndex}\`);
3910
+ whereParams.push(opValue);
3911
+ paramIndex++;
3912
+ }
3913
+ break;
3914
+ case '$jsonbHasAllKeys':
3915
+ if (Array.isArray(opValue) && opValue.length > 0) {
3916
+ whereParts.push(\`"\${key}" ?& $\${paramIndex}\`);
3917
+ whereParams.push(opValue);
3918
+ paramIndex++;
3919
+ }
3920
+ break;
3921
+ case '$jsonbPath':
3922
+ const pathConfig = opValue;
3923
+ const pathKeys = pathConfig.path;
3924
+ const pathOperator = pathConfig.operator || '$eq';
3925
+ const pathValue = pathConfig.value;
3926
+
3927
+ if (!Array.isArray(pathKeys) || pathKeys.length === 0) {
3928
+ break;
3929
+ }
3930
+
3931
+ // Build path accessor: metadata->'user'->'preferences'->>'theme'
3932
+ // Use -> for all keys except the last one, use ->> for the last to get text
3933
+ const pathParts = pathKeys.slice(0, -1);
3934
+ const lastKey = pathKeys[pathKeys.length - 1];
3935
+
3936
+ let pathExpr = \`"\${key}"\`;
3937
+ for (const part of pathParts) {
3938
+ pathExpr += \`->'\${part}'\`;
3939
+ }
3940
+ pathExpr += \`->>'\${lastKey}'\`;
3941
+
3942
+ // Apply the operator
3943
+ switch (pathOperator) {
3944
+ case '$eq':
3945
+ whereParts.push(\`\${pathExpr} = $\${paramIndex}\`);
3946
+ whereParams.push(String(pathValue));
3947
+ paramIndex++;
3948
+ break;
3949
+ case '$ne':
3950
+ whereParts.push(\`\${pathExpr} != $\${paramIndex}\`);
3951
+ whereParams.push(String(pathValue));
3952
+ paramIndex++;
3953
+ break;
3954
+ case '$gt':
3955
+ whereParts.push(\`(\${pathExpr})::numeric > $\${paramIndex}\`);
3956
+ whereParams.push(pathValue);
3957
+ paramIndex++;
3958
+ break;
3959
+ case '$gte':
3960
+ whereParts.push(\`(\${pathExpr})::numeric >= $\${paramIndex}\`);
3961
+ whereParams.push(pathValue);
3962
+ paramIndex++;
3963
+ break;
3964
+ case '$lt':
3965
+ whereParts.push(\`(\${pathExpr})::numeric < $\${paramIndex}\`);
3966
+ whereParams.push(pathValue);
3967
+ paramIndex++;
3968
+ break;
3969
+ case '$lte':
3970
+ whereParts.push(\`(\${pathExpr})::numeric <= $\${paramIndex}\`);
3971
+ whereParams.push(pathValue);
3972
+ paramIndex++;
3973
+ break;
3974
+ case '$like':
3975
+ whereParts.push(\`\${pathExpr} LIKE $\${paramIndex}\`);
3976
+ whereParams.push(pathValue);
3977
+ paramIndex++;
3978
+ break;
3979
+ case '$ilike':
3980
+ whereParts.push(\`\${pathExpr} ILIKE $\${paramIndex}\`);
3981
+ whereParams.push(pathValue);
3982
+ paramIndex++;
3983
+ break;
3984
+ }
3985
+ break;
3739
3986
  }
3740
3987
  }
3741
3988
  } else if (value === null) {
@@ -3791,17 +4038,51 @@ function buildWhereClause(
3791
4038
  }
3792
4039
 
3793
4040
  /**
3794
- * LIST operation - Get multiple records with optional filters
4041
+ * Get distance operator for vector similarity search
4042
+ */
4043
+ function getVectorDistanceOperator(metric?: string): string {
4044
+ switch (metric) {
4045
+ case "l2":
4046
+ return "<->";
4047
+ case "inner":
4048
+ return "<#>";
4049
+ case "cosine":
4050
+ default:
4051
+ return "<=>";
4052
+ }
4053
+ }
4054
+
4055
+ /**
4056
+ * LIST operation - Get multiple records with optional filters and vector search
3795
4057
  */
3796
4058
  export async function listRecords(
3797
4059
  ctx: OperationContext,
3798
- params: { where?: any; limit?: number; offset?: number; include?: any; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[] }
4060
+ params: {
4061
+ where?: any;
4062
+ limit?: number;
4063
+ offset?: number;
4064
+ include?: any;
4065
+ orderBy?: string | string[];
4066
+ order?: "asc" | "desc" | ("asc" | "desc")[];
4067
+ vector?: {
4068
+ field: string;
4069
+ query: number[];
4070
+ metric?: "cosine" | "l2" | "inner";
4071
+ maxDistance?: number;
4072
+ };
4073
+ }
3799
4074
  ): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
3800
4075
  try {
3801
- const { where: whereClause, limit = 50, offset = 0, include, orderBy, order } = params;
4076
+ const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector } = params;
4077
+
4078
+ // Get distance operator if vector search
4079
+ const distanceOp = vector ? getVectorDistanceOperator(vector.metric) : "";
3802
4080
 
3803
- // Build WHERE clause
3804
- let paramIndex = 1;
4081
+ // Add vector to params array if present
4082
+ const queryParams: any[] = vector ? [JSON.stringify(vector.query)] : [];
4083
+
4084
+ // Build WHERE clause for SELECT/UPDATE queries (with vector as $1 if present)
4085
+ let paramIndex = vector ? 2 : 1;
3805
4086
  const whereParts: string[] = [];
3806
4087
  let whereParams: any[] = [];
3807
4088
 
@@ -3820,11 +4101,47 @@ export async function listRecords(
3820
4101
  }
3821
4102
  }
3822
4103
 
4104
+ // Add vector distance threshold filter if specified
4105
+ if (vector?.maxDistance !== undefined) {
4106
+ whereParts.push(\`("\${vector.field}" \${distanceOp} ($1)::vector) < \${vector.maxDistance}\`);
4107
+ }
4108
+
3823
4109
  const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
3824
4110
 
4111
+ // Build WHERE clause for COUNT query (may need different param indices)
4112
+ let countWhereSQL = whereSQL;
4113
+ let countParams = whereParams;
4114
+
4115
+ if (vector && vector.maxDistance === undefined && whereParams.length > 0) {
4116
+ // COUNT query doesn't use vector, so rebuild WHERE without vector offset
4117
+ const countWhereParts: string[] = [];
4118
+ if (ctx.softDeleteColumn) {
4119
+ countWhereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
4120
+ }
4121
+ if (whereClause) {
4122
+ const result = buildWhereClause(whereClause, 1); // Start at $1 for count
4123
+ if (result.sql) {
4124
+ countWhereParts.push(result.sql);
4125
+ countParams = result.params;
4126
+ }
4127
+ }
4128
+ countWhereSQL = countWhereParts.length > 0 ? \`WHERE \${countWhereParts.join(" AND ")}\` : "";
4129
+ } else if (vector?.maxDistance !== undefined) {
4130
+ // COUNT query includes vector for maxDistance filter
4131
+ countParams = [...queryParams, ...whereParams];
4132
+ }
4133
+
4134
+ // Build SELECT clause
4135
+ const selectClause = vector
4136
+ ? \`*, ("\${vector.field}" \${distanceOp} ($1)::vector) AS _distance\`
4137
+ : "*";
4138
+
3825
4139
  // Build ORDER BY clause
3826
4140
  let orderBySQL = "";
3827
- if (orderBy) {
4141
+ if (vector) {
4142
+ // For vector search, always order by distance
4143
+ orderBySQL = \`ORDER BY "\${vector.field}" \${distanceOp} ($1)::vector\`;
4144
+ } else if (orderBy) {
3828
4145
  const columns = Array.isArray(orderBy) ? orderBy : [orderBy];
3829
4146
  const directions = Array.isArray(order) ? order : (order ? Array(columns.length).fill(order) : Array(columns.length).fill("asc"));
3830
4147
 
@@ -3839,16 +4156,16 @@ export async function listRecords(
3839
4156
  // Add limit and offset params
3840
4157
  const limitParam = \`$\${paramIndex}\`;
3841
4158
  const offsetParam = \`$\${paramIndex + 1}\`;
3842
- const allParams = [...whereParams, limit, offset];
4159
+ const allParams = [...queryParams, ...whereParams, limit, offset];
3843
4160
 
3844
4161
  // Get total count for pagination
3845
- const countText = \`SELECT COUNT(*) FROM "\${ctx.table}" \${whereSQL}\`;
3846
- log.debug(\`LIST \${ctx.table} COUNT SQL:\`, countText, "params:", whereParams);
3847
- const countResult = await ctx.pg.query(countText, whereParams);
4162
+ const countText = \`SELECT COUNT(*) FROM "\${ctx.table}" \${countWhereSQL}\`;
4163
+ log.debug(\`LIST \${ctx.table} COUNT SQL:\`, countText, "params:", countParams);
4164
+ const countResult = await ctx.pg.query(countText, countParams);
3848
4165
  const total = parseInt(countResult.rows[0].count, 10);
3849
4166
 
3850
4167
  // Get paginated data
3851
- const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
4168
+ const text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
3852
4169
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
3853
4170
 
3854
4171
  const { rows } = await ctx.pg.query(text, allParams);
@@ -3,6 +3,7 @@ export type Column = {
3
3
  pgType: string;
4
4
  nullable: boolean;
5
5
  hasDefault: boolean;
6
+ vectorDimension?: number;
6
7
  };
7
8
  export type ForeignKey = {
8
9
  from: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.15.6",
3
+ "version": "0.16.0",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {