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 +124 -2
- package/dist/cli.js +346 -29
- package/dist/index.js +346 -29
- package/dist/introspect.d.ts +1 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
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
|
-
|
|
2530
|
+
const pgType = (r.udt_name ?? r.data_type).toLowerCase();
|
|
2531
|
+
const col = {
|
|
2521
2532
|
name: r.column_name,
|
|
2522
|
-
pgType
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
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<
|
|
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<
|
|
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
|
-
*
|
|
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: {
|
|
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
|
-
//
|
|
4633
|
-
|
|
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 (
|
|
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}" \${
|
|
4675
|
-
log.debug(\`LIST \${ctx.table} COUNT SQL:\`, countText, "params:",
|
|
4676
|
-
const countResult = await ctx.pg.query(countText,
|
|
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
|
|
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
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
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
|
-
|
|
1701
|
+
const pgType = (r.udt_name ?? r.data_type).toLowerCase();
|
|
1702
|
+
const col = {
|
|
1692
1703
|
name: r.column_name,
|
|
1693
|
-
pgType
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
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<
|
|
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<
|
|
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
|
-
*
|
|
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: {
|
|
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
|
-
//
|
|
3804
|
-
|
|
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 (
|
|
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}" \${
|
|
3846
|
-
log.debug(\`LIST \${ctx.table} COUNT SQL:\`, countText, "params:",
|
|
3847
|
-
const countResult = await ctx.pg.query(countText,
|
|
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
|
|
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);
|
package/dist/introspect.d.ts
CHANGED