turbine-orm 0.11.0 → 0.13.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/cli/migrate.js +25 -28
- package/dist/cjs/dialect.js +74 -0
- package/dist/cjs/query/builder.js +30 -23
- package/dist/cjs/schema-sql.js +64 -61
- package/dist/cli/migrate.d.ts +6 -1
- package/dist/cli/migrate.js +25 -28
- package/dist/dialect.d.ts +107 -0
- package/dist/dialect.js +74 -0
- package/dist/index.d.ts +2 -2
- package/dist/query/builder.js +30 -23
- package/dist/query/index.d.ts +1 -1
- package/dist/schema-sql.d.ts +7 -2
- package/dist/schema-sql.js +64 -61
- package/package.json +3 -3
package/dist/schema-sql.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Also provides diff and push commands for syncing schema to a live database.
|
|
6
6
|
*/
|
|
7
7
|
import pg from 'pg';
|
|
8
|
-
import {
|
|
8
|
+
import { postgresDialect } from './dialect.js';
|
|
9
9
|
import { camelToSnake } from './schema.js';
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// SQL Generation — SchemaDef → CREATE TABLE statements
|
|
@@ -16,7 +16,8 @@ import { camelToSnake } from './schema.js';
|
|
|
16
16
|
* Returns CREATE TABLE statements (in dependency order based on references)
|
|
17
17
|
* followed by CREATE INDEX statements for foreign key columns.
|
|
18
18
|
*/
|
|
19
|
-
export function schemaToSQL(schema) {
|
|
19
|
+
export function schemaToSQL(schema, options) {
|
|
20
|
+
const dialect = options?.dialect ?? postgresDialect;
|
|
20
21
|
const statements = [];
|
|
21
22
|
// Topologically sort tables by their foreign key references
|
|
22
23
|
const sorted = topologicalSort(schema);
|
|
@@ -24,12 +25,12 @@ export function schemaToSQL(schema) {
|
|
|
24
25
|
// Generate CREATE TABLE statements
|
|
25
26
|
for (const tableName of sorted) {
|
|
26
27
|
const table = schema.tables[tableName];
|
|
27
|
-
statements.push(generateCreateTable(table, resolveRef));
|
|
28
|
+
statements.push(generateCreateTable(table, resolveRef, dialect));
|
|
28
29
|
}
|
|
29
30
|
// Generate CREATE INDEX for foreign key columns
|
|
30
31
|
for (const tableName of sorted) {
|
|
31
32
|
const table = schema.tables[tableName];
|
|
32
|
-
const indexes = generateForeignKeyIndexes(table);
|
|
33
|
+
const indexes = generateForeignKeyIndexes(table, dialect);
|
|
33
34
|
statements.push(...indexes);
|
|
34
35
|
}
|
|
35
36
|
return statements;
|
|
@@ -125,42 +126,28 @@ function topologicalSort(schema) {
|
|
|
125
126
|
* to their snake_case DDL form, so users can write either camelCase JS
|
|
126
127
|
* accessor names or snake_case DDL names.
|
|
127
128
|
*/
|
|
128
|
-
function generateCreateTable(table, resolveRef) {
|
|
129
|
+
function generateCreateTable(table, resolveRef, dialect = postgresDialect) {
|
|
129
130
|
const tableName = table.name;
|
|
130
131
|
const columnDefs = [];
|
|
131
132
|
const compositePk = table.primaryKey && table.primaryKey.length > 0 ? table.primaryKey : null;
|
|
132
133
|
for (const [fieldName, config] of Object.entries(table.columns)) {
|
|
133
|
-
columnDefs.push(generateColumnDef(fieldName, config, resolveRef));
|
|
134
|
+
columnDefs.push(generateColumnDef(fieldName, config, resolveRef, dialect));
|
|
134
135
|
}
|
|
135
136
|
// Append a table-level PRIMARY KEY constraint when a composite PK is set.
|
|
136
137
|
if (compositePk) {
|
|
137
|
-
const cols = compositePk.map((c) =>
|
|
138
|
-
columnDefs.push(
|
|
138
|
+
const cols = compositePk.map((c) => dialect.quoteIdentifier(camelToSnake(c)));
|
|
139
|
+
columnDefs.push(dialect.buildPrimaryKeyConstraint(cols));
|
|
139
140
|
}
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
return dialect.buildCreateTableStatement({
|
|
142
|
+
table: dialect.quoteIdentifier(tableName),
|
|
143
|
+
definitions: columnDefs,
|
|
144
|
+
});
|
|
142
145
|
}
|
|
143
146
|
/**
|
|
144
147
|
* Generate a single column definition line (e.g. "id BIGSERIAL PRIMARY KEY").
|
|
145
148
|
*/
|
|
146
|
-
function generateColumnDef(fieldName, config, resolveRef) {
|
|
149
|
+
function generateColumnDef(fieldName, config, resolveRef, dialect = postgresDialect) {
|
|
147
150
|
const snakeName = camelToSnake(fieldName);
|
|
148
|
-
const parts = [quoteIdent(snakeName)];
|
|
149
|
-
// Type
|
|
150
|
-
if (config.type === 'VARCHAR' && config.maxLength != null) {
|
|
151
|
-
parts.push(`VARCHAR(${config.maxLength})`);
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
parts.push(config.type);
|
|
155
|
-
}
|
|
156
|
-
// PRIMARY KEY
|
|
157
|
-
if (config.isPrimaryKey) {
|
|
158
|
-
parts.push('PRIMARY KEY');
|
|
159
|
-
}
|
|
160
|
-
// UNIQUE (only if not primary key — PK is implicitly unique)
|
|
161
|
-
if (config.isUnique && !config.isPrimaryKey) {
|
|
162
|
-
parts.push('UNIQUE');
|
|
163
|
-
}
|
|
164
151
|
// NOT NULL — serial types are implicitly NOT NULL, but explicit is fine.
|
|
165
152
|
// A column is NOT NULL if:
|
|
166
153
|
// 1. Explicitly marked .notNull(), OR
|
|
@@ -169,25 +156,36 @@ function generateColumnDef(fieldName, config, resolveRef) {
|
|
|
169
156
|
// A column is left nullable if .nullable() was called.
|
|
170
157
|
const isSerial = config.type === 'BIGSERIAL';
|
|
171
158
|
const implicitNotNull = isSerial || config.isPrimaryKey;
|
|
172
|
-
|
|
173
|
-
parts.push('NOT NULL');
|
|
174
|
-
}
|
|
159
|
+
const notNull = config.isNotNull && !implicitNotNull;
|
|
175
160
|
// DEFAULT
|
|
161
|
+
let defaultValue;
|
|
176
162
|
if (config.defaultValue != null) {
|
|
177
|
-
|
|
178
|
-
parts.push(`DEFAULT ${sqlDefault}`);
|
|
163
|
+
defaultValue = normalizeDefault(config.defaultValue);
|
|
179
164
|
}
|
|
180
165
|
// REFERENCES — resolve the raw table name through the optional resolver so
|
|
181
166
|
// both camelCase accessor names and snake_case DDL names work.
|
|
167
|
+
let references;
|
|
182
168
|
if (config.referencesTarget) {
|
|
183
169
|
const refParts = config.referencesTarget.split('.');
|
|
184
170
|
if (refParts.length === 2) {
|
|
185
171
|
const rawTable = refParts[0];
|
|
186
172
|
const refTable = resolveRef ? resolveRef(rawTable) : rawTable;
|
|
187
|
-
|
|
173
|
+
references = {
|
|
174
|
+
table: dialect.quoteIdentifier(refTable),
|
|
175
|
+
column: dialect.quoteIdentifier(refParts[1]),
|
|
176
|
+
};
|
|
188
177
|
}
|
|
189
178
|
}
|
|
190
|
-
return
|
|
179
|
+
return dialect.buildColumnDefinition({
|
|
180
|
+
name: dialect.quoteIdentifier(snakeName),
|
|
181
|
+
type: config.type,
|
|
182
|
+
maxLength: config.maxLength,
|
|
183
|
+
primaryKey: config.isPrimaryKey,
|
|
184
|
+
unique: config.isUnique,
|
|
185
|
+
notNull,
|
|
186
|
+
defaultValue,
|
|
187
|
+
references,
|
|
188
|
+
});
|
|
191
189
|
}
|
|
192
190
|
/**
|
|
193
191
|
* Normalize a default value from the user's schema definition to valid SQL.
|
|
@@ -227,13 +225,17 @@ function normalizeDefault(val) {
|
|
|
227
225
|
* Generate CREATE INDEX statements for foreign key columns.
|
|
228
226
|
* Only generates indexes for columns that have a REFERENCES clause.
|
|
229
227
|
*/
|
|
230
|
-
function generateForeignKeyIndexes(table) {
|
|
228
|
+
function generateForeignKeyIndexes(table, dialect = postgresDialect) {
|
|
231
229
|
const indexes = [];
|
|
232
230
|
for (const [fieldName, config] of Object.entries(table.columns)) {
|
|
233
231
|
if (config.referencesTarget) {
|
|
234
232
|
const snakeName = camelToSnake(fieldName);
|
|
235
233
|
const indexName = `idx_${table.name}_${snakeName}`;
|
|
236
|
-
indexes.push(
|
|
234
|
+
indexes.push(dialect.buildCreateIndexStatement({
|
|
235
|
+
name: dialect.quoteIdentifier(indexName),
|
|
236
|
+
table: dialect.quoteIdentifier(table.name),
|
|
237
|
+
columns: [dialect.quoteIdentifier(snakeName)],
|
|
238
|
+
}));
|
|
237
239
|
}
|
|
238
240
|
}
|
|
239
241
|
return indexes;
|
|
@@ -245,6 +247,7 @@ function generateForeignKeyIndexes(table) {
|
|
|
245
247
|
* DDL is needed to make the database match the schema definition.
|
|
246
248
|
*/
|
|
247
249
|
export async function schemaDiff(schema, connectionString) {
|
|
250
|
+
const dialect = postgresDialect;
|
|
248
251
|
const client = new pg.Client({ connectionString });
|
|
249
252
|
await client.connect();
|
|
250
253
|
try {
|
|
@@ -304,11 +307,11 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
304
307
|
const ddlName = tableDef.name;
|
|
305
308
|
if (!existingTables.has(ddlName)) {
|
|
306
309
|
result.create.push(tableDef);
|
|
307
|
-
result.statements.push(generateCreateTable(tableDef, resolveRef));
|
|
308
|
-
const fkIndexes = generateForeignKeyIndexes(tableDef);
|
|
310
|
+
result.statements.push(generateCreateTable(tableDef, resolveRef, dialect));
|
|
311
|
+
const fkIndexes = generateForeignKeyIndexes(tableDef, dialect);
|
|
309
312
|
result.statements.push(...fkIndexes);
|
|
310
313
|
// Reverse: DROP TABLE (with indexes — they drop automatically)
|
|
311
|
-
result.reverseStatements.unshift(`DROP TABLE IF EXISTS ${
|
|
314
|
+
result.reverseStatements.unshift(`DROP TABLE IF EXISTS ${dialect.quoteIdentifier(ddlName)} CASCADE;`);
|
|
312
315
|
}
|
|
313
316
|
}
|
|
314
317
|
// Tables to drop (in DB but not in schema)
|
|
@@ -332,9 +335,9 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
332
335
|
const dbCol = dbCols[snakeName];
|
|
333
336
|
if (!dbCol) {
|
|
334
337
|
// Column exists in schema but not in DB — ADD COLUMN
|
|
335
|
-
const colDef = generateColumnDef(fieldName, config, resolveRef);
|
|
336
|
-
const sql = `ALTER TABLE ${
|
|
337
|
-
const reverseSql = `ALTER TABLE ${
|
|
338
|
+
const colDef = generateColumnDef(fieldName, config, resolveRef, dialect);
|
|
339
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ADD COLUMN ${colDef};`;
|
|
340
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP COLUMN ${dialect.quoteIdentifier(snakeName)};`;
|
|
338
341
|
alterDef.columns.push({ column: snakeName, action: 'add', sql, reverseSql });
|
|
339
342
|
result.statements.push(sql);
|
|
340
343
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -345,8 +348,8 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
345
348
|
if (expectedUdt && dbCol.udtName !== expectedUdt) {
|
|
346
349
|
const sqlType = config.type === 'VARCHAR' && config.maxLength ? `VARCHAR(${config.maxLength})` : config.type;
|
|
347
350
|
const oldSqlType = udtToSqlType(dbCol.udtName, dbCol.maxLength);
|
|
348
|
-
const sql = `ALTER TABLE ${
|
|
349
|
-
const reverseSql = `ALTER TABLE ${
|
|
351
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${sqlType} USING ${dialect.quoteIdentifier(snakeName)}::${sqlType};`;
|
|
352
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${oldSqlType} USING ${dialect.quoteIdentifier(snakeName)}::${oldSqlType};`;
|
|
350
353
|
alterDef.columns.push({ column: snakeName, action: 'alter_type', sql, reverseSql });
|
|
351
354
|
result.statements.push(sql);
|
|
352
355
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -355,15 +358,15 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
355
358
|
const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || config.type === 'BIGSERIAL';
|
|
356
359
|
const isCurrentlyNullable = dbCol.isNullable;
|
|
357
360
|
if (shouldBeNotNull && isCurrentlyNullable && !config.isNullable) {
|
|
358
|
-
const sql = `ALTER TABLE ${
|
|
359
|
-
const reverseSql = `ALTER TABLE ${
|
|
361
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET NOT NULL;`;
|
|
362
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} DROP NOT NULL;`;
|
|
360
363
|
alterDef.columns.push({ column: snakeName, action: 'set_not_null', sql, reverseSql });
|
|
361
364
|
result.statements.push(sql);
|
|
362
365
|
result.reverseStatements.unshift(reverseSql);
|
|
363
366
|
}
|
|
364
367
|
else if (!shouldBeNotNull && !isCurrentlyNullable && config.isNullable) {
|
|
365
|
-
const sql = `ALTER TABLE ${
|
|
366
|
-
const reverseSql = `ALTER TABLE ${
|
|
368
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} DROP NOT NULL;`;
|
|
369
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET NOT NULL;`;
|
|
367
370
|
alterDef.columns.push({ column: snakeName, action: 'drop_not_null', sql, reverseSql });
|
|
368
371
|
result.statements.push(sql);
|
|
369
372
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -375,16 +378,16 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
375
378
|
const dbDefault = dbCol.columnDefault;
|
|
376
379
|
if (schemaDefault && !dbDefault) {
|
|
377
380
|
// Schema has default, DB doesn't
|
|
378
|
-
const sql = `ALTER TABLE ${
|
|
379
|
-
const reverseSql = `ALTER TABLE ${
|
|
381
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET DEFAULT ${schemaDefault};`;
|
|
382
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} DROP DEFAULT;`;
|
|
380
383
|
alterDef.columns.push({ column: snakeName, action: 'set_default', sql, reverseSql });
|
|
381
384
|
result.statements.push(sql);
|
|
382
385
|
result.reverseStatements.unshift(reverseSql);
|
|
383
386
|
}
|
|
384
387
|
else if (!schemaDefault && dbDefault && !isSequenceDefault(dbDefault)) {
|
|
385
388
|
// DB has a non-sequence default, schema doesn't
|
|
386
|
-
const sql = `ALTER TABLE ${
|
|
387
|
-
const reverseSql = `ALTER TABLE ${
|
|
389
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} DROP DEFAULT;`;
|
|
390
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET DEFAULT ${dbDefault};`;
|
|
388
391
|
alterDef.columns.push({ column: snakeName, action: 'drop_default', sql, reverseSql });
|
|
389
392
|
result.statements.push(sql);
|
|
390
393
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -394,8 +397,8 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
394
397
|
!isSequenceDefault(dbDefault) &&
|
|
395
398
|
!defaultsMatch(schemaDefault, dbDefault)) {
|
|
396
399
|
// Both have defaults but they differ
|
|
397
|
-
const sql = `ALTER TABLE ${
|
|
398
|
-
const reverseSql = `ALTER TABLE ${
|
|
400
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET DEFAULT ${schemaDefault};`;
|
|
401
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET DEFAULT ${dbDefault};`;
|
|
399
402
|
alterDef.columns.push({ column: snakeName, action: 'set_default', sql, reverseSql });
|
|
400
403
|
result.statements.push(sql);
|
|
401
404
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -407,16 +410,16 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
407
410
|
const wantsUnique = config.isUnique === true;
|
|
408
411
|
if (wantsUnique && !hasDbUnique) {
|
|
409
412
|
const constraintName = `${tableName}_${snakeName}_key`;
|
|
410
|
-
const sql = `ALTER TABLE ${
|
|
411
|
-
const reverseSql = `ALTER TABLE ${
|
|
413
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ADD CONSTRAINT ${dialect.quoteIdentifier(constraintName)} UNIQUE (${dialect.quoteIdentifier(snakeName)});`;
|
|
414
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP CONSTRAINT ${dialect.quoteIdentifier(constraintName)};`;
|
|
412
415
|
alterDef.columns.push({ column: snakeName, action: 'add_unique', sql, reverseSql });
|
|
413
416
|
result.statements.push(sql);
|
|
414
417
|
result.reverseStatements.unshift(reverseSql);
|
|
415
418
|
}
|
|
416
419
|
else if (!wantsUnique && hasDbUnique) {
|
|
417
420
|
const constraintName = tableUniques[snakeName];
|
|
418
|
-
const sql = `ALTER TABLE ${
|
|
419
|
-
const reverseSql = `ALTER TABLE ${
|
|
421
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP CONSTRAINT ${dialect.quoteIdentifier(constraintName)};`;
|
|
422
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ADD CONSTRAINT ${dialect.quoteIdentifier(constraintName)} UNIQUE (${dialect.quoteIdentifier(snakeName)});`;
|
|
420
423
|
alterDef.columns.push({ column: snakeName, action: 'drop_unique', sql, reverseSql });
|
|
421
424
|
result.statements.push(sql);
|
|
422
425
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -427,7 +430,7 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
427
430
|
for (const dbColName of Object.keys(dbCols)) {
|
|
428
431
|
const hasField = Object.entries(tableDef.columns).some(([fieldName]) => camelToSnake(fieldName) === dbColName);
|
|
429
432
|
if (!hasField) {
|
|
430
|
-
const sql = `ALTER TABLE ${
|
|
433
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP COLUMN ${dialect.quoteIdentifier(dbColName)};`;
|
|
431
434
|
const reverseSql = `-- Cannot auto-reverse DROP COLUMN for "${dbColName}" — add it back manually`;
|
|
432
435
|
alterDef.columns.push({ column: dbColName, action: 'drop', sql, reverseSql });
|
|
433
436
|
// Don't auto-add drops to statements for safety — user must opt in
|
|
@@ -560,8 +563,8 @@ export async function schemaPush(schema, connectionString, options = {}) {
|
|
|
560
563
|
* Generate the full DDL as a single formatted string.
|
|
561
564
|
* Useful for printing or saving to a .sql file.
|
|
562
565
|
*/
|
|
563
|
-
export function schemaToSQLString(schema) {
|
|
564
|
-
const statements = schemaToSQL(schema);
|
|
566
|
+
export function schemaToSQLString(schema, options) {
|
|
567
|
+
const statements = schemaToSQL(schema, options);
|
|
565
568
|
return `${statements.join('\n\n')}\n`;
|
|
566
569
|
}
|
|
567
570
|
//# sourceMappingURL=schema-sql.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"main": "./dist/cjs/index.js",
|
|
29
29
|
"types": "./dist/index.d.ts",
|
|
30
30
|
"bin": {
|
|
31
|
-
"turbine": "
|
|
31
|
+
"turbine": "dist/cli/index.js"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"dist",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"homepage": "https://turbineorm.dev",
|
|
102
102
|
"repository": {
|
|
103
103
|
"type": "git",
|
|
104
|
-
"url": "https://github.com/zvndev/turbine-orm.git"
|
|
104
|
+
"url": "git+https://github.com/zvndev/turbine-orm.git"
|
|
105
105
|
},
|
|
106
106
|
"bugs": {
|
|
107
107
|
"url": "https://github.com/zvndev/turbine-orm/issues"
|