turbine-orm 0.13.2 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/dialect.js +6 -4
- package/dist/cjs/generate.js +9 -1
- package/dist/cjs/introspect.js +14 -4
- package/dist/cjs/query/builder.js +7 -6
- package/dist/dialect.d.ts +3 -3
- package/dist/dialect.js +6 -4
- package/dist/generate.js +9 -1
- package/dist/introspect.js +15 -5
- package/dist/query/builder.js +7 -6
- package/dist/schema.d.ts +9 -3
- package/package.json +1 -1
package/dist/cjs/dialect.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.postgresDialect = void 0;
|
|
11
11
|
const errors_js_1 = require("./errors.js");
|
|
12
|
+
const schema_js_1 = require("./schema.js");
|
|
12
13
|
/** PostgreSQL implementation of the dialect contract. */
|
|
13
14
|
exports.postgresDialect = {
|
|
14
15
|
name: 'postgresql',
|
|
@@ -72,10 +73,11 @@ exports.postgresDialect = {
|
|
|
72
73
|
.map((col, i) => `${leftRef}.${this.quoteIdentifier(col)} = ${rightRef}.${this.quoteIdentifier(rightCols[i])}`)
|
|
73
74
|
.join(' AND ');
|
|
74
75
|
},
|
|
75
|
-
typeToTypeScript(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
typeToTypeScript(dialectType, nullable) {
|
|
77
|
+
return (0, schema_js_1.pgTypeToTs)(dialectType, nullable);
|
|
78
|
+
},
|
|
79
|
+
arrayType(baseType) {
|
|
80
|
+
return (0, schema_js_1.pgArrayType)(baseType);
|
|
79
81
|
},
|
|
80
82
|
buildColumnType(input) {
|
|
81
83
|
if (input.type === 'VARCHAR' && input.maxLength != null) {
|
package/dist/cjs/generate.js
CHANGED
|
@@ -218,7 +218,13 @@ function generateMetadata(schema) {
|
|
|
218
218
|
// dateColumns
|
|
219
219
|
const dateCols = [...table.dateColumns];
|
|
220
220
|
lines.push(` dateColumns: new Set([${dateCols.map((c) => `'${escSQ(c)}'`).join(', ')}]),`);
|
|
221
|
-
// pgTypes
|
|
221
|
+
// dialectTypes + pgTypes (pgTypes is kept for backwards compatibility)
|
|
222
|
+
const dialectTypes = table.dialectTypes ?? table.pgTypes;
|
|
223
|
+
lines.push(' dialectTypes: {');
|
|
224
|
+
for (const [col, dialectType] of Object.entries(dialectTypes)) {
|
|
225
|
+
lines.push(` ${quoteIfNeeded(col)}: '${escSQ(dialectType)}',`);
|
|
226
|
+
}
|
|
227
|
+
lines.push(' },');
|
|
222
228
|
lines.push(' pgTypes: {');
|
|
223
229
|
for (const [col, pgType] of Object.entries(table.pgTypes)) {
|
|
224
230
|
lines.push(` ${quoteIfNeeded(col)}: '${escSQ(pgType)}',`);
|
|
@@ -392,11 +398,13 @@ function serializeColumn(col) {
|
|
|
392
398
|
const parts = [
|
|
393
399
|
`name: '${escSQ(col.name)}'`,
|
|
394
400
|
`field: '${escSQ(col.field)}'`,
|
|
401
|
+
`dialectType: '${escSQ(col.dialectType ?? col.pgType)}'`,
|
|
395
402
|
`pgType: '${escSQ(col.pgType)}'`,
|
|
396
403
|
`tsType: '${escSQ(col.tsType)}'`,
|
|
397
404
|
`nullable: ${col.nullable}`,
|
|
398
405
|
`hasDefault: ${col.hasDefault}`,
|
|
399
406
|
`isArray: ${col.isArray}`,
|
|
407
|
+
`arrayType: '${escSQ(col.arrayType ?? col.pgArrayType)}'`,
|
|
400
408
|
`pgArrayType: '${escSQ(col.pgArrayType)}'`,
|
|
401
409
|
];
|
|
402
410
|
if (col.maxLength !== undefined)
|
package/dist/cjs/introspect.js
CHANGED
|
@@ -14,6 +14,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.introspect = introspect;
|
|
16
16
|
const pg_1 = __importDefault(require("pg"));
|
|
17
|
+
const dialect_js_1 = require("./dialect.js");
|
|
17
18
|
const schema_js_1 = require("./schema.js");
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// SQL queries (all parameterized, no interpolation)
|
|
@@ -101,6 +102,7 @@ const SQL_ENUMS = `
|
|
|
101
102
|
// ---------------------------------------------------------------------------
|
|
102
103
|
async function introspect(options) {
|
|
103
104
|
const schema = options.schema ?? 'public';
|
|
105
|
+
const dialect = dialect_js_1.postgresDialect;
|
|
104
106
|
const pool = new pg_1.default.Pool({
|
|
105
107
|
connectionString: options.connectionString,
|
|
106
108
|
max: 1,
|
|
@@ -137,15 +139,20 @@ async function introspect(options) {
|
|
|
137
139
|
const isNullable = row.is_nullable === 'YES';
|
|
138
140
|
const isArray = row.data_type === 'ARRAY';
|
|
139
141
|
const baseType = isArray ? row.udt_name.slice(1) : row.udt_name;
|
|
142
|
+
const dialectType = row.udt_name;
|
|
143
|
+
const arrayType = dialect.arrayType?.(baseType) ?? 'text[]';
|
|
140
144
|
const col = {
|
|
141
145
|
name: row.column_name,
|
|
142
146
|
field: (0, schema_js_1.snakeToCamel)(row.column_name),
|
|
143
|
-
|
|
144
|
-
|
|
147
|
+
dialectType,
|
|
148
|
+
pgType: dialectType,
|
|
149
|
+
tsType: dialect.typeToTypeScript?.(isArray ? dialectType : baseType, isNullable) ??
|
|
150
|
+
(0, schema_js_1.pgTypeToTs)(isArray ? dialectType : baseType, isNullable),
|
|
145
151
|
nullable: isNullable,
|
|
146
152
|
hasDefault: row.column_default !== null,
|
|
147
153
|
isArray,
|
|
148
|
-
|
|
154
|
+
arrayType,
|
|
155
|
+
pgArrayType: arrayType,
|
|
149
156
|
maxLength: row.character_maximum_length ?? undefined,
|
|
150
157
|
};
|
|
151
158
|
if (!columnsByTable.has(tableName))
|
|
@@ -281,14 +288,16 @@ async function introspect(options) {
|
|
|
281
288
|
const columnMap = {};
|
|
282
289
|
const reverseColumnMap = {};
|
|
283
290
|
const dateColumns = new Set();
|
|
291
|
+
const dialectTypes = {};
|
|
284
292
|
const pgTypes = {};
|
|
285
293
|
const allColumns = [];
|
|
286
294
|
for (const col of columns) {
|
|
287
295
|
columnMap[col.field] = col.name;
|
|
288
296
|
reverseColumnMap[col.name] = col.field;
|
|
289
297
|
allColumns.push(col.name);
|
|
298
|
+
dialectTypes[col.name] = col.dialectType ?? col.pgType;
|
|
290
299
|
pgTypes[col.name] = col.pgType;
|
|
291
|
-
const baseType = col.isArray ? col.pgType.slice(1) : col.pgType;
|
|
300
|
+
const baseType = col.isArray ? (col.dialectType ?? col.pgType).slice(1) : (col.dialectType ?? col.pgType);
|
|
292
301
|
if ((0, schema_js_1.isDateType)(baseType)) {
|
|
293
302
|
dateColumns.add(col.name);
|
|
294
303
|
}
|
|
@@ -299,6 +308,7 @@ async function introspect(options) {
|
|
|
299
308
|
columnMap,
|
|
300
309
|
reverseColumnMap,
|
|
301
310
|
dateColumns,
|
|
311
|
+
dialectTypes,
|
|
302
312
|
pgTypes,
|
|
303
313
|
allColumns,
|
|
304
314
|
primaryKey: pkByTable.get(tableName) ?? [],
|
|
@@ -153,8 +153,8 @@ class QueryInterface {
|
|
|
153
153
|
this.columnPgTypeMap = new Map();
|
|
154
154
|
this.columnArrayTypeMap = new Map();
|
|
155
155
|
for (const col of this.tableMeta.columns) {
|
|
156
|
-
this.columnPgTypeMap.set(col.name, col.pgType);
|
|
157
|
-
this.columnArrayTypeMap.set(col.name, col.pgArrayType);
|
|
156
|
+
this.columnPgTypeMap.set(col.name, col.dialectType ?? col.pgType);
|
|
157
|
+
this.columnArrayTypeMap.set(col.name, col.arrayType ?? col.pgArrayType);
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
/** Quote an identifier through the active SQL dialect. */
|
|
@@ -2680,12 +2680,13 @@ class QueryInterface {
|
|
|
2680
2680
|
const arrayType = this.columnArrayTypeMap.get(column);
|
|
2681
2681
|
if (arrayType)
|
|
2682
2682
|
return arrayType;
|
|
2683
|
-
// Fallback heuristic for unknown columns
|
|
2683
|
+
// Fallback heuristic for unknown columns, routed through the active dialect
|
|
2684
|
+
// so non-Postgres packages can supply their own bulk-insert cast shape.
|
|
2684
2685
|
if (column === 'id' || column.endsWith('_id'))
|
|
2685
|
-
return '
|
|
2686
|
+
return this.dialect.arrayType?.('int8') ?? 'text[]';
|
|
2686
2687
|
if (column.endsWith('_at'))
|
|
2687
|
-
return 'timestamptz[]';
|
|
2688
|
-
return 'text[]';
|
|
2688
|
+
return this.dialect.arrayType?.('timestamptz') ?? 'text[]';
|
|
2689
|
+
return this.dialect.arrayType?.('text') ?? 'text[]';
|
|
2689
2690
|
}
|
|
2690
2691
|
}
|
|
2691
2692
|
exports.QueryInterface = QueryInterface;
|
package/dist/dialect.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* PostgreSQL-native by default, but query generation now depends on this
|
|
6
6
|
* contract for the SQL primitives that vary across MySQL and SQLite.
|
|
7
7
|
*/
|
|
8
|
-
import type
|
|
8
|
+
import { type SchemaMetadata } from './schema.js';
|
|
9
9
|
export type DialectName = 'postgresql' | 'mysql' | 'sqlite' | (string & {});
|
|
10
10
|
export interface InsertStatementInput {
|
|
11
11
|
/** SQL-ready quoted table name. */
|
|
@@ -125,8 +125,8 @@ export interface Dialect {
|
|
|
125
125
|
buildJsonPathExtract(column: string, pathParamRef: string): string;
|
|
126
126
|
/** Build a correlation clause across single or composite keys. */
|
|
127
127
|
buildCorrelation(leftRef: string, leftColumns: string | string[], rightRef: string, rightColumns: string | string[]): string;
|
|
128
|
-
/**
|
|
129
|
-
typeToTypeScript(dialectType: string, nullable: boolean): string;
|
|
128
|
+
/** Optional type mapping hook for code generation/introspection. */
|
|
129
|
+
typeToTypeScript?(dialectType: string, nullable: boolean): string;
|
|
130
130
|
/** Optional array-cast hook for bulk insert implementations. */
|
|
131
131
|
arrayType?(baseType: string): string;
|
|
132
132
|
/** Map a schema-builder column type to dialect DDL. */
|
package/dist/dialect.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* contract for the SQL primitives that vary across MySQL and SQLite.
|
|
7
7
|
*/
|
|
8
8
|
import { ValidationError } from './errors.js';
|
|
9
|
+
import { pgArrayType, pgTypeToTs } from './schema.js';
|
|
9
10
|
/** PostgreSQL implementation of the dialect contract. */
|
|
10
11
|
export const postgresDialect = {
|
|
11
12
|
name: 'postgresql',
|
|
@@ -69,10 +70,11 @@ export const postgresDialect = {
|
|
|
69
70
|
.map((col, i) => `${leftRef}.${this.quoteIdentifier(col)} = ${rightRef}.${this.quoteIdentifier(rightCols[i])}`)
|
|
70
71
|
.join(' AND ');
|
|
71
72
|
},
|
|
72
|
-
typeToTypeScript(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
typeToTypeScript(dialectType, nullable) {
|
|
74
|
+
return pgTypeToTs(dialectType, nullable);
|
|
75
|
+
},
|
|
76
|
+
arrayType(baseType) {
|
|
77
|
+
return pgArrayType(baseType);
|
|
76
78
|
},
|
|
77
79
|
buildColumnType(input) {
|
|
78
80
|
if (input.type === 'VARCHAR' && input.maxLength != null) {
|
package/dist/generate.js
CHANGED
|
@@ -214,7 +214,13 @@ function generateMetadata(schema) {
|
|
|
214
214
|
// dateColumns
|
|
215
215
|
const dateCols = [...table.dateColumns];
|
|
216
216
|
lines.push(` dateColumns: new Set([${dateCols.map((c) => `'${escSQ(c)}'`).join(', ')}]),`);
|
|
217
|
-
// pgTypes
|
|
217
|
+
// dialectTypes + pgTypes (pgTypes is kept for backwards compatibility)
|
|
218
|
+
const dialectTypes = table.dialectTypes ?? table.pgTypes;
|
|
219
|
+
lines.push(' dialectTypes: {');
|
|
220
|
+
for (const [col, dialectType] of Object.entries(dialectTypes)) {
|
|
221
|
+
lines.push(` ${quoteIfNeeded(col)}: '${escSQ(dialectType)}',`);
|
|
222
|
+
}
|
|
223
|
+
lines.push(' },');
|
|
218
224
|
lines.push(' pgTypes: {');
|
|
219
225
|
for (const [col, pgType] of Object.entries(table.pgTypes)) {
|
|
220
226
|
lines.push(` ${quoteIfNeeded(col)}: '${escSQ(pgType)}',`);
|
|
@@ -388,11 +394,13 @@ function serializeColumn(col) {
|
|
|
388
394
|
const parts = [
|
|
389
395
|
`name: '${escSQ(col.name)}'`,
|
|
390
396
|
`field: '${escSQ(col.field)}'`,
|
|
397
|
+
`dialectType: '${escSQ(col.dialectType ?? col.pgType)}'`,
|
|
391
398
|
`pgType: '${escSQ(col.pgType)}'`,
|
|
392
399
|
`tsType: '${escSQ(col.tsType)}'`,
|
|
393
400
|
`nullable: ${col.nullable}`,
|
|
394
401
|
`hasDefault: ${col.hasDefault}`,
|
|
395
402
|
`isArray: ${col.isArray}`,
|
|
403
|
+
`arrayType: '${escSQ(col.arrayType ?? col.pgArrayType)}'`,
|
|
396
404
|
`pgArrayType: '${escSQ(col.pgArrayType)}'`,
|
|
397
405
|
];
|
|
398
406
|
if (col.maxLength !== undefined)
|
package/dist/introspect.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* This is the foundation of `npx turbine generate`.
|
|
9
9
|
*/
|
|
10
10
|
import pg from 'pg';
|
|
11
|
-
import {
|
|
11
|
+
import { postgresDialect } from './dialect.js';
|
|
12
|
+
import { isDateType, pgTypeToTs, singularize, snakeToCamel, } from './schema.js';
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
// SQL queries (all parameterized, no interpolation)
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
@@ -95,6 +96,7 @@ const SQL_ENUMS = `
|
|
|
95
96
|
// ---------------------------------------------------------------------------
|
|
96
97
|
export async function introspect(options) {
|
|
97
98
|
const schema = options.schema ?? 'public';
|
|
99
|
+
const dialect = postgresDialect;
|
|
98
100
|
const pool = new pg.Pool({
|
|
99
101
|
connectionString: options.connectionString,
|
|
100
102
|
max: 1,
|
|
@@ -131,15 +133,20 @@ export async function introspect(options) {
|
|
|
131
133
|
const isNullable = row.is_nullable === 'YES';
|
|
132
134
|
const isArray = row.data_type === 'ARRAY';
|
|
133
135
|
const baseType = isArray ? row.udt_name.slice(1) : row.udt_name;
|
|
136
|
+
const dialectType = row.udt_name;
|
|
137
|
+
const arrayType = dialect.arrayType?.(baseType) ?? 'text[]';
|
|
134
138
|
const col = {
|
|
135
139
|
name: row.column_name,
|
|
136
140
|
field: snakeToCamel(row.column_name),
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
dialectType,
|
|
142
|
+
pgType: dialectType,
|
|
143
|
+
tsType: dialect.typeToTypeScript?.(isArray ? dialectType : baseType, isNullable) ??
|
|
144
|
+
pgTypeToTs(isArray ? dialectType : baseType, isNullable),
|
|
139
145
|
nullable: isNullable,
|
|
140
146
|
hasDefault: row.column_default !== null,
|
|
141
147
|
isArray,
|
|
142
|
-
|
|
148
|
+
arrayType,
|
|
149
|
+
pgArrayType: arrayType,
|
|
143
150
|
maxLength: row.character_maximum_length ?? undefined,
|
|
144
151
|
};
|
|
145
152
|
if (!columnsByTable.has(tableName))
|
|
@@ -275,14 +282,16 @@ export async function introspect(options) {
|
|
|
275
282
|
const columnMap = {};
|
|
276
283
|
const reverseColumnMap = {};
|
|
277
284
|
const dateColumns = new Set();
|
|
285
|
+
const dialectTypes = {};
|
|
278
286
|
const pgTypes = {};
|
|
279
287
|
const allColumns = [];
|
|
280
288
|
for (const col of columns) {
|
|
281
289
|
columnMap[col.field] = col.name;
|
|
282
290
|
reverseColumnMap[col.name] = col.field;
|
|
283
291
|
allColumns.push(col.name);
|
|
292
|
+
dialectTypes[col.name] = col.dialectType ?? col.pgType;
|
|
284
293
|
pgTypes[col.name] = col.pgType;
|
|
285
|
-
const baseType = col.isArray ? col.pgType.slice(1) : col.pgType;
|
|
294
|
+
const baseType = col.isArray ? (col.dialectType ?? col.pgType).slice(1) : (col.dialectType ?? col.pgType);
|
|
286
295
|
if (isDateType(baseType)) {
|
|
287
296
|
dateColumns.add(col.name);
|
|
288
297
|
}
|
|
@@ -293,6 +302,7 @@ export async function introspect(options) {
|
|
|
293
302
|
columnMap,
|
|
294
303
|
reverseColumnMap,
|
|
295
304
|
dateColumns,
|
|
305
|
+
dialectTypes,
|
|
296
306
|
pgTypes,
|
|
297
307
|
allColumns,
|
|
298
308
|
primaryKey: pkByTable.get(tableName) ?? [],
|
package/dist/query/builder.js
CHANGED
|
@@ -150,8 +150,8 @@ export class QueryInterface {
|
|
|
150
150
|
this.columnPgTypeMap = new Map();
|
|
151
151
|
this.columnArrayTypeMap = new Map();
|
|
152
152
|
for (const col of this.tableMeta.columns) {
|
|
153
|
-
this.columnPgTypeMap.set(col.name, col.pgType);
|
|
154
|
-
this.columnArrayTypeMap.set(col.name, col.pgArrayType);
|
|
153
|
+
this.columnPgTypeMap.set(col.name, col.dialectType ?? col.pgType);
|
|
154
|
+
this.columnArrayTypeMap.set(col.name, col.arrayType ?? col.pgArrayType);
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
/** Quote an identifier through the active SQL dialect. */
|
|
@@ -2677,12 +2677,13 @@ export class QueryInterface {
|
|
|
2677
2677
|
const arrayType = this.columnArrayTypeMap.get(column);
|
|
2678
2678
|
if (arrayType)
|
|
2679
2679
|
return arrayType;
|
|
2680
|
-
// Fallback heuristic for unknown columns
|
|
2680
|
+
// Fallback heuristic for unknown columns, routed through the active dialect
|
|
2681
|
+
// so non-Postgres packages can supply their own bulk-insert cast shape.
|
|
2681
2682
|
if (column === 'id' || column.endsWith('_id'))
|
|
2682
|
-
return '
|
|
2683
|
+
return this.dialect.arrayType?.('int8') ?? 'text[]';
|
|
2683
2684
|
if (column.endsWith('_at'))
|
|
2684
|
-
return 'timestamptz[]';
|
|
2685
|
-
return 'text[]';
|
|
2685
|
+
return this.dialect.arrayType?.('timestamptz') ?? 'text[]';
|
|
2686
|
+
return this.dialect.arrayType?.('text') ?? 'text[]';
|
|
2686
2687
|
}
|
|
2687
2688
|
}
|
|
2688
2689
|
//# sourceMappingURL=builder.js.map
|
package/dist/schema.d.ts
CHANGED
|
@@ -21,7 +21,9 @@ export interface TableMetadata {
|
|
|
21
21
|
reverseColumnMap: Record<string, string>;
|
|
22
22
|
/** snake_case columns that are timestamp/date types (need Date parsing) */
|
|
23
23
|
dateColumns: Set<string>;
|
|
24
|
-
/** snake_case column →
|
|
24
|
+
/** snake_case column → dialect-native database type. */
|
|
25
|
+
dialectTypes?: Record<string, string>;
|
|
26
|
+
/** snake_case column → Postgres type for UNNEST casts. Back-compat alias for dialectTypes. */
|
|
25
27
|
pgTypes: Record<string, string>;
|
|
26
28
|
/** All snake_case column names in ordinal order */
|
|
27
29
|
allColumns: string[];
|
|
@@ -39,7 +41,9 @@ export interface ColumnMetadata {
|
|
|
39
41
|
name: string;
|
|
40
42
|
/** camelCase field name for TypeScript */
|
|
41
43
|
field: string;
|
|
42
|
-
/**
|
|
44
|
+
/** Dialect-native database type (e.g. PostgreSQL 'int8', MySQL 'bigint', SQLite 'INTEGER'). */
|
|
45
|
+
dialectType?: string;
|
|
46
|
+
/** Postgres base type (e.g. 'int8', 'text', 'timestamptz'). Back-compat alias for dialectType. */
|
|
43
47
|
pgType: string;
|
|
44
48
|
/** TypeScript type string (e.g. 'number', 'string', 'Date') */
|
|
45
49
|
tsType: string;
|
|
@@ -49,7 +53,9 @@ export interface ColumnMetadata {
|
|
|
49
53
|
hasDefault: boolean;
|
|
50
54
|
/** Whether this is an array column */
|
|
51
55
|
isArray: boolean;
|
|
52
|
-
/**
|
|
56
|
+
/** Dialect-specific array/bulk-insert type token when needed. */
|
|
57
|
+
arrayType?: string;
|
|
58
|
+
/** Postgres array type for UNNEST (e.g. 'bigint[]'). Back-compat alias for arrayType. */
|
|
53
59
|
pgArrayType: string;
|
|
54
60
|
/** Max character length (for varchar) */
|
|
55
61
|
maxLength?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. 1 dependency, ~110KB",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|