turbine-orm 0.29.0 → 0.31.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 +1 -1
- package/dist/cjs/cli/index.js +5 -0
- package/dist/cjs/cli/mcp.js +22 -92
- package/dist/cjs/client.js +47 -6
- package/dist/cjs/generate.js +71 -25
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/introspect.js +350 -120
- package/dist/cjs/mssql.js +42 -136
- package/dist/cjs/mysql.js +16 -129
- package/dist/cjs/optional-peer-import.cjs +122 -0
- package/dist/cjs/powdb.js +579 -89
- package/dist/cjs/powql.js +56 -26
- package/dist/cjs/query/builder.js +601 -86
- package/dist/cjs/query/filters.js +80 -2
- package/dist/cjs/schema-metadata.js +316 -0
- package/dist/cjs/sqlite.js +8 -89
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/mcp.d.ts +18 -0
- package/dist/cli/mcp.js +22 -93
- package/dist/client.d.ts +19 -2
- package/dist/client.js +47 -6
- package/dist/generate.d.ts +16 -4
- package/dist/generate.js +71 -25
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/introspect.d.ts +94 -1
- package/dist/introspect.js +345 -120
- package/dist/mssql.js +40 -104
- package/dist/mysql.js +14 -97
- package/dist/optional-peer-import.cjs +89 -0
- package/dist/optional-peer-import.d.cts +53 -0
- package/dist/powdb.d.ts +118 -23
- package/dist/powdb.js +574 -88
- package/dist/powql.d.ts +6 -0
- package/dist/powql.js +58 -28
- package/dist/query/builder.d.ts +145 -8
- package/dist/query/builder.js +602 -87
- package/dist/query/deferred.d.ts +7 -2
- package/dist/query/filters.d.ts +46 -1
- package/dist/query/filters.js +76 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +85 -11
- package/dist/schema-metadata.d.ts +77 -0
- package/dist/schema-metadata.js +313 -0
- package/dist/schema.d.ts +10 -0
- package/dist/sqlite.js +9 -90
- package/package.json +3 -3
package/dist/mssql.js
CHANGED
|
@@ -92,7 +92,9 @@
|
|
|
92
92
|
import { TurbineClient } from './client.js';
|
|
93
93
|
import { postgresDialect, } from './dialect.js';
|
|
94
94
|
import { ConnectionError, RelationError, UnsupportedFeatureError, ValidationError } from './errors.js';
|
|
95
|
-
import {
|
|
95
|
+
import { deriveEngineRelations } from './introspect.js';
|
|
96
|
+
import importOptionalPeer from './optional-peer-import.cjs';
|
|
97
|
+
import { camelToSnake, isDateType, normalizeKeyColumns, snakeToCamel, } from './schema.js';
|
|
96
98
|
// ---------------------------------------------------------------------------
|
|
97
99
|
// SQL Server / connection limits
|
|
98
100
|
// ---------------------------------------------------------------------------
|
|
@@ -743,11 +745,32 @@ function buildForJsonSubquery(dialect, ctx) {
|
|
|
743
745
|
if (hasOrder) {
|
|
744
746
|
const orderBy = orderEntries
|
|
745
747
|
.map(([k, dir]) => {
|
|
746
|
-
|
|
748
|
+
// FOR JSON nested ordering supports plain directions and { sort }
|
|
749
|
+
// specs only. Object shapes the core builder compiles with params
|
|
750
|
+
// (JSON-path / vector / relation ordering) must throw here: the
|
|
751
|
+
// shared param-collect mirror is gated on the native path, so a
|
|
752
|
+
// silently-ignored object would desync SQL text from params.
|
|
753
|
+
let rawDir = dir;
|
|
754
|
+
if (typeof dir === 'object' && dir !== null) {
|
|
755
|
+
const sortValue = dir.sort;
|
|
756
|
+
if (typeof sortValue !== 'string') {
|
|
757
|
+
throw new ValidationError(`[turbine] Nested orderBy on "${k}" (table "${targetTable}"): only plain directions and ` +
|
|
758
|
+
`{ sort } specs are supported inside a relation orderBy on SQL Server.`);
|
|
759
|
+
}
|
|
760
|
+
if (dir.nulls !== undefined) {
|
|
761
|
+
throw new UnsupportedFeatureError('NULLS FIRST/LAST ordering', 'sqlserver', 'Explicit nulls placement in orderBy is only available on PostgreSQL and SQLite.');
|
|
762
|
+
}
|
|
763
|
+
rawDir = sortValue;
|
|
764
|
+
}
|
|
765
|
+
// columnMap-first resolution (camelToSnake fallback): matches the
|
|
766
|
+
// core builder's nested orderBy path so camelCase-named DB columns
|
|
767
|
+
// resolve on SQL Server too.
|
|
768
|
+
const col = targetMeta.columnMap[k] ?? camelToSnake(k);
|
|
747
769
|
if (!targetMeta.allColumns.includes(col)) {
|
|
748
|
-
throw new ValidationError(`[turbine] Unknown
|
|
770
|
+
throw new ValidationError(`[turbine] Unknown field "${k}" in orderBy on table "${targetTable}". ` +
|
|
771
|
+
`Known fields: ${Object.keys(targetMeta.columnMap).join(', ') || '(none)'}.`);
|
|
749
772
|
}
|
|
750
|
-
const safeDir = String(
|
|
773
|
+
const safeDir = String(rawDir).toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
751
774
|
return `${a}.${q(col)} ${safeDir}`;
|
|
752
775
|
})
|
|
753
776
|
.join(', ');
|
|
@@ -853,103 +876,15 @@ function buildForJsonManyToMany(dialect, ctx, h) {
|
|
|
853
876
|
}
|
|
854
877
|
const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
|
|
855
878
|
/**
|
|
856
|
-
* Derive
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
*
|
|
879
|
+
* Derive relations from the FK list via the SHARED introspection pipeline
|
|
880
|
+
* (`deriveEngineRelations` → `buildRelationsFromForeignKeys` +
|
|
881
|
+
* `addAutoManyToManyRelations` in introspect.ts), so this engine derives
|
|
882
|
+
* IDENTICAL relation names to `turbine generate` against Postgres for the
|
|
883
|
+
* same logical schema — legacy-first naming, per-column disambiguation, and
|
|
884
|
+
* collision resolution against scalar column fields included.
|
|
860
885
|
*/
|
|
861
886
|
function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
|
|
862
|
-
|
|
863
|
-
const fkCounts = new Map();
|
|
864
|
-
for (const fk of foreignKeys) {
|
|
865
|
-
const key = `${fk.sourceTable}->${fk.targetTable}`;
|
|
866
|
-
fkCounts.set(key, (fkCounts.get(key) ?? 0) + 1);
|
|
867
|
-
}
|
|
868
|
-
const relationsByTable = new Map();
|
|
869
|
-
for (const fk of foreignKeys) {
|
|
870
|
-
if (!tableSet.has(fk.targetTable))
|
|
871
|
-
continue;
|
|
872
|
-
const needsDisambiguation = (fkCounts.get(`${fk.sourceTable}->${fk.targetTable}`) ?? 0) > 1;
|
|
873
|
-
const foreignKey = fk.sourceColumns.length === 1 ? fk.sourceColumns[0] : fk.sourceColumns;
|
|
874
|
-
const referenceKey = fk.targetColumns.length === 1 ? fk.targetColumns[0] : fk.targetColumns;
|
|
875
|
-
const belongsToName = needsDisambiguation && fk.sourceColumns.length === 1
|
|
876
|
-
? snakeToCamel(fk.sourceColumns[0].replace(/_id$/, ''))
|
|
877
|
-
: singularize(snakeToCamel(fk.targetTable));
|
|
878
|
-
if (!relationsByTable.has(fk.sourceTable))
|
|
879
|
-
relationsByTable.set(fk.sourceTable, {});
|
|
880
|
-
relationsByTable.get(fk.sourceTable)[belongsToName] = {
|
|
881
|
-
type: 'belongsTo',
|
|
882
|
-
name: belongsToName,
|
|
883
|
-
from: fk.sourceTable,
|
|
884
|
-
to: fk.targetTable,
|
|
885
|
-
foreignKey,
|
|
886
|
-
referenceKey,
|
|
887
|
-
};
|
|
888
|
-
const hasManyName = needsDisambiguation && fk.sourceColumns.length === 1
|
|
889
|
-
? snakeToCamel(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
|
|
890
|
-
: snakeToCamel(fk.sourceTable);
|
|
891
|
-
if (!relationsByTable.has(fk.targetTable))
|
|
892
|
-
relationsByTable.set(fk.targetTable, {});
|
|
893
|
-
relationsByTable.get(fk.targetTable)[hasManyName] = {
|
|
894
|
-
type: 'hasMany',
|
|
895
|
-
name: hasManyName,
|
|
896
|
-
from: fk.targetTable,
|
|
897
|
-
to: fk.sourceTable,
|
|
898
|
-
foreignKey,
|
|
899
|
-
referenceKey,
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
// Conservative many-to-many auto-detection (additive): a table J is a pure
|
|
903
|
-
// junction iff PK is exactly two columns, exactly two single-column FKs whose
|
|
904
|
-
// source columns ARE the PK, two distinct target tables, and no payload columns.
|
|
905
|
-
for (const tableName of tableNames) {
|
|
906
|
-
const pk = pkByTable.get(tableName) ?? [];
|
|
907
|
-
if (pk.length !== 2)
|
|
908
|
-
continue;
|
|
909
|
-
const tableFks = foreignKeys.filter((fk) => fk.sourceTable === tableName);
|
|
910
|
-
if (tableFks.length !== 2)
|
|
911
|
-
continue;
|
|
912
|
-
if (tableFks.some((fk) => fk.sourceColumns.length !== 1))
|
|
913
|
-
continue;
|
|
914
|
-
const fkCols = tableFks.map((fk) => fk.sourceColumns[0]);
|
|
915
|
-
const pkSet = new Set(pk);
|
|
916
|
-
if (!fkCols.every((c) => pkSet.has(c)))
|
|
917
|
-
continue;
|
|
918
|
-
if (new Set(fkCols).size !== 2)
|
|
919
|
-
continue;
|
|
920
|
-
const [fkA, fkB] = tableFks;
|
|
921
|
-
if (fkA.targetTable === fkB.targetTable)
|
|
922
|
-
continue;
|
|
923
|
-
const jCols = (columnsByTable.get(tableName) ?? []).map((c) => c.name);
|
|
924
|
-
if (jCols.length !== 2)
|
|
925
|
-
continue;
|
|
926
|
-
const addM2M = (self, other) => {
|
|
927
|
-
const sourceTbl = self.targetTable;
|
|
928
|
-
const targetTbl = other.targetTable;
|
|
929
|
-
const relName = snakeToCamel(targetTbl);
|
|
930
|
-
if (!relationsByTable.has(sourceTbl))
|
|
931
|
-
relationsByTable.set(sourceTbl, {});
|
|
932
|
-
const existing = relationsByTable.get(sourceTbl);
|
|
933
|
-
if (existing[relName])
|
|
934
|
-
return;
|
|
935
|
-
existing[relName] = {
|
|
936
|
-
type: 'manyToMany',
|
|
937
|
-
name: relName,
|
|
938
|
-
from: sourceTbl,
|
|
939
|
-
to: targetTbl,
|
|
940
|
-
referenceKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
941
|
-
foreignKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
942
|
-
through: {
|
|
943
|
-
table: tableName,
|
|
944
|
-
sourceKey: self.sourceColumns[0],
|
|
945
|
-
targetKey: other.sourceColumns[0],
|
|
946
|
-
},
|
|
947
|
-
};
|
|
948
|
-
};
|
|
949
|
-
addM2M(fkA, fkB);
|
|
950
|
-
addM2M(fkB, fkA);
|
|
951
|
-
}
|
|
952
|
-
return relationsByTable;
|
|
887
|
+
return deriveEngineRelations(tableNames, foreignKeys, pkByTable, columnsByTable);
|
|
953
888
|
}
|
|
954
889
|
/**
|
|
955
890
|
* Introspect a SQL Server database into the same {@link SchemaMetadata} shape the
|
|
@@ -1196,11 +1131,12 @@ async function loadMssql() {
|
|
|
1196
1131
|
let mod;
|
|
1197
1132
|
try {
|
|
1198
1133
|
// `mssql` ships no bundled type declarations (it needs @types/mssql, which
|
|
1199
|
-
// Turbine deliberately does not depend on) — the structural MssqlModule
|
|
1200
|
-
// is our typed surface
|
|
1201
|
-
//
|
|
1202
|
-
|
|
1203
|
-
|
|
1134
|
+
// Turbine deliberately does not depend on) — the structural MssqlModule
|
|
1135
|
+
// above is our typed surface; the helper returns `unknown` so no TS7016.
|
|
1136
|
+
// Via the .cts helper so the CJS build keeps a path to a REAL dynamic
|
|
1137
|
+
// import() even if a future mssql major goes ESM-only (the CommonJS pass
|
|
1138
|
+
// transpiles a plain `import()` here into `require()`).
|
|
1139
|
+
mod = (await importOptionalPeer('mssql'));
|
|
1204
1140
|
}
|
|
1205
1141
|
catch (err) {
|
|
1206
1142
|
throw new ConnectionError("[turbine] turbine-orm/mssql requires the optional peer dependency 'mssql'. Install it: npm i mssql. " +
|
package/dist/mysql.js
CHANGED
|
@@ -65,7 +65,9 @@
|
|
|
65
65
|
import { TurbineClient } from './client.js';
|
|
66
66
|
import { postgresDialect, } from './dialect.js';
|
|
67
67
|
import { ConnectionError, UnsupportedFeatureError } from './errors.js';
|
|
68
|
-
import {
|
|
68
|
+
import { deriveEngineRelations } from './introspect.js';
|
|
69
|
+
import importOptionalPeer from './optional-peer-import.cjs';
|
|
70
|
+
import { isDateType, snakeToCamel, } from './schema.js';
|
|
69
71
|
// ---------------------------------------------------------------------------
|
|
70
72
|
// Value coercion (params in)
|
|
71
73
|
// ---------------------------------------------------------------------------
|
|
@@ -555,103 +557,15 @@ export const mysqlDialect = {
|
|
|
555
557
|
};
|
|
556
558
|
const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
|
|
557
559
|
/**
|
|
558
|
-
* Derive
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
560
|
+
* Derive relations from the FK list via the SHARED introspection pipeline
|
|
561
|
+
* (`deriveEngineRelations` → `buildRelationsFromForeignKeys` +
|
|
562
|
+
* `addAutoManyToManyRelations` in introspect.ts), so this engine derives
|
|
563
|
+
* IDENTICAL relation names to `turbine generate` against Postgres for the
|
|
564
|
+
* same logical schema — legacy-first naming, per-column disambiguation, and
|
|
565
|
+
* collision resolution against scalar column fields included.
|
|
562
566
|
*/
|
|
563
567
|
function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
|
|
564
|
-
|
|
565
|
-
const fkCounts = new Map();
|
|
566
|
-
for (const fk of foreignKeys) {
|
|
567
|
-
const key = `${fk.sourceTable}->${fk.targetTable}`;
|
|
568
|
-
fkCounts.set(key, (fkCounts.get(key) ?? 0) + 1);
|
|
569
|
-
}
|
|
570
|
-
const relationsByTable = new Map();
|
|
571
|
-
for (const fk of foreignKeys) {
|
|
572
|
-
if (!tableSet.has(fk.targetTable))
|
|
573
|
-
continue;
|
|
574
|
-
const needsDisambiguation = (fkCounts.get(`${fk.sourceTable}->${fk.targetTable}`) ?? 0) > 1;
|
|
575
|
-
const foreignKey = fk.sourceColumns.length === 1 ? fk.sourceColumns[0] : fk.sourceColumns;
|
|
576
|
-
const referenceKey = fk.targetColumns.length === 1 ? fk.targetColumns[0] : fk.targetColumns;
|
|
577
|
-
const belongsToName = needsDisambiguation && fk.sourceColumns.length === 1
|
|
578
|
-
? snakeToCamel(fk.sourceColumns[0].replace(/_id$/, ''))
|
|
579
|
-
: singularize(snakeToCamel(fk.targetTable));
|
|
580
|
-
if (!relationsByTable.has(fk.sourceTable))
|
|
581
|
-
relationsByTable.set(fk.sourceTable, {});
|
|
582
|
-
relationsByTable.get(fk.sourceTable)[belongsToName] = {
|
|
583
|
-
type: 'belongsTo',
|
|
584
|
-
name: belongsToName,
|
|
585
|
-
from: fk.sourceTable,
|
|
586
|
-
to: fk.targetTable,
|
|
587
|
-
foreignKey,
|
|
588
|
-
referenceKey,
|
|
589
|
-
};
|
|
590
|
-
const hasManyName = needsDisambiguation && fk.sourceColumns.length === 1
|
|
591
|
-
? snakeToCamel(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
|
|
592
|
-
: snakeToCamel(fk.sourceTable);
|
|
593
|
-
if (!relationsByTable.has(fk.targetTable))
|
|
594
|
-
relationsByTable.set(fk.targetTable, {});
|
|
595
|
-
relationsByTable.get(fk.targetTable)[hasManyName] = {
|
|
596
|
-
type: 'hasMany',
|
|
597
|
-
name: hasManyName,
|
|
598
|
-
from: fk.targetTable,
|
|
599
|
-
to: fk.sourceTable,
|
|
600
|
-
foreignKey,
|
|
601
|
-
referenceKey,
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
// Conservative many-to-many auto-detection (additive): a table J is a pure
|
|
605
|
-
// junction iff PK is exactly two columns, exactly two single-column FKs whose
|
|
606
|
-
// source columns ARE the PK, two distinct target tables, and no payload columns.
|
|
607
|
-
for (const tableName of tableNames) {
|
|
608
|
-
const pk = pkByTable.get(tableName) ?? [];
|
|
609
|
-
if (pk.length !== 2)
|
|
610
|
-
continue;
|
|
611
|
-
const tableFks = foreignKeys.filter((fk) => fk.sourceTable === tableName);
|
|
612
|
-
if (tableFks.length !== 2)
|
|
613
|
-
continue;
|
|
614
|
-
if (tableFks.some((fk) => fk.sourceColumns.length !== 1))
|
|
615
|
-
continue;
|
|
616
|
-
const fkCols = tableFks.map((fk) => fk.sourceColumns[0]);
|
|
617
|
-
const pkSet = new Set(pk);
|
|
618
|
-
if (!fkCols.every((c) => pkSet.has(c)))
|
|
619
|
-
continue;
|
|
620
|
-
if (new Set(fkCols).size !== 2)
|
|
621
|
-
continue;
|
|
622
|
-
const [fkA, fkB] = tableFks;
|
|
623
|
-
if (fkA.targetTable === fkB.targetTable)
|
|
624
|
-
continue;
|
|
625
|
-
const jCols = (columnsByTable.get(tableName) ?? []).map((c) => c.name);
|
|
626
|
-
if (jCols.length !== 2)
|
|
627
|
-
continue;
|
|
628
|
-
const addM2M = (self, other) => {
|
|
629
|
-
const sourceTbl = self.targetTable;
|
|
630
|
-
const targetTbl = other.targetTable;
|
|
631
|
-
const relName = snakeToCamel(targetTbl);
|
|
632
|
-
if (!relationsByTable.has(sourceTbl))
|
|
633
|
-
relationsByTable.set(sourceTbl, {});
|
|
634
|
-
const existing = relationsByTable.get(sourceTbl);
|
|
635
|
-
if (existing[relName])
|
|
636
|
-
return;
|
|
637
|
-
existing[relName] = {
|
|
638
|
-
type: 'manyToMany',
|
|
639
|
-
name: relName,
|
|
640
|
-
from: sourceTbl,
|
|
641
|
-
to: targetTbl,
|
|
642
|
-
referenceKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
643
|
-
foreignKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
644
|
-
through: {
|
|
645
|
-
table: tableName,
|
|
646
|
-
sourceKey: self.sourceColumns[0],
|
|
647
|
-
targetKey: other.sourceColumns[0],
|
|
648
|
-
},
|
|
649
|
-
};
|
|
650
|
-
};
|
|
651
|
-
addM2M(fkA, fkB);
|
|
652
|
-
addM2M(fkB, fkA);
|
|
653
|
-
}
|
|
654
|
-
return relationsByTable;
|
|
568
|
+
return deriveEngineRelations(tableNames, foreignKeys, pkByTable, columnsByTable);
|
|
655
569
|
}
|
|
656
570
|
/**
|
|
657
571
|
* Introspect a MySQL database into the same {@link SchemaMetadata} shape the
|
|
@@ -898,7 +812,10 @@ function parseMysqlConfig(connectionString) {
|
|
|
898
812
|
async function loadCreatePool() {
|
|
899
813
|
let mod;
|
|
900
814
|
try {
|
|
901
|
-
|
|
815
|
+
// Via the .cts helper so the CJS build keeps a path to a REAL dynamic
|
|
816
|
+
// import() even if a future mysql2 major goes ESM-only (the CommonJS pass
|
|
817
|
+
// transpiles a plain `import()` here into `require()`).
|
|
818
|
+
mod = (await importOptionalPeer('mysql2/promise'));
|
|
902
819
|
}
|
|
903
820
|
catch (err) {
|
|
904
821
|
throw new ConnectionError("[turbine] turbine-orm/mysql requires the optional peer dependency 'mysql2'. Install it: npm i mysql2. " +
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* True dynamic `import()` for the optional peer dependencies (`mysql2`,
|
|
4
|
+
* `mssql`, `@zvndev/powdb-client`, `@zvndev/powdb-embedded`) — safe in BOTH
|
|
5
|
+
* build outputs, including for peers that are ESM-only.
|
|
6
|
+
*
|
|
7
|
+
* THE PROBLEM THIS FILE SOLVES (the `@zvndev/powdb-client` ≥ 0.9 CJS break):
|
|
8
|
+
* the engine subpaths load their optional peers with dynamic `import()` so the
|
|
9
|
+
* peers stay out of the static graph. The ESM build (`tsconfig.json`, module
|
|
10
|
+
* NodeNext) emits that `import()` verbatim. The CJS build (`tsconfig.cjs.json`,
|
|
11
|
+
* module CommonJS) however TRANSPILES `import()` into
|
|
12
|
+
* `Promise.resolve().then(() => require(...))` — and `require()` of an
|
|
13
|
+
* ESM-only package (no `require` export condition, e.g. powdb-client ≥ 0.9)
|
|
14
|
+
* throws `ERR_PACKAGE_PATH_NOT_EXPORTED`, breaking every CJS consumer.
|
|
15
|
+
*
|
|
16
|
+
* TypeScript offers no way to preserve `import()` under `module: CommonJS`,
|
|
17
|
+
* and the CJS pass cannot switch to `module: NodeNext` (the root package.json
|
|
18
|
+
* says `"type": "module"`, so NodeNext would classify every `.ts` source as
|
|
19
|
+
* ESM and emit ESM into dist/cjs). A `.cts` file is the escape hatch: it is
|
|
20
|
+
* CommonJS-format by extension regardless of package `type`, so under the ESM
|
|
21
|
+
* pass (NodeNext) it compiles to `dist/optional-peer-import.cjs` — a CommonJS
|
|
22
|
+
* file whose `import()` SURVIVES transpilation (NodeNext preserves dynamic
|
|
23
|
+
* import in CJS files precisely because it is the only way CJS can load ESM).
|
|
24
|
+
*
|
|
25
|
+
* That gives the published package two copies of this module:
|
|
26
|
+
* - `dist/optional-peer-import.cjs` (ESM pass, NodeNext) — real `import()`
|
|
27
|
+
* - `dist/cjs/optional-peer-import.cjs` (CJS pass, CommonJS) — lowered to `require()`
|
|
28
|
+
*
|
|
29
|
+
* The lowered copy works fine for CJS-loadable peers (`mysql2`, `mssql`, older
|
|
30
|
+
* powdb peers). When it hits an ESM-only peer, the `require()` fails with a
|
|
31
|
+
* recognizable code and this function falls back to delegating the load to the
|
|
32
|
+
* sibling NodeNext copy one directory up (`../optional-peer-import.cjs`) —
|
|
33
|
+
* which is a plain CommonJS file (loadable by `require()` on every supported
|
|
34
|
+
* Node) whose real `import()` then loads the ESM peer. The ESM-pass copy has
|
|
35
|
+
* no such sibling; its lazy `require` throws and the original error surfaces,
|
|
36
|
+
* so the fallback can never recurse.
|
|
37
|
+
*
|
|
38
|
+
* Keep this module dependency-free and side-effect-free: it must be loadable
|
|
39
|
+
* from both module systems on every supported Node (≥ 20) without pulling in
|
|
40
|
+
* anything else.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Does this error mean "the module exists but cannot be loaded via
|
|
44
|
+
* `require()` because it is ESM-only"? These are the only failures worth
|
|
45
|
+
* retrying through a real `import()`; anything else (not installed, throw on
|
|
46
|
+
* init, …) is rethrown untouched so callers keep the original diagnostics.
|
|
47
|
+
*/
|
|
48
|
+
function isEsmOnlyLoadError(err) {
|
|
49
|
+
const code = err?.code;
|
|
50
|
+
return (code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || // exports map has no `require` condition
|
|
51
|
+
code === 'ERR_REQUIRE_ESM' || // require() of an ES module (pre-require(esm) Node)
|
|
52
|
+
code === 'ERR_REQUIRE_ASYNC_MODULE' // require(esm) of a module with top-level await
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Dynamically import an optional peer dependency. In the ESM build this is a
|
|
57
|
+
* plain `import()`. In the CJS build the first attempt is a transpiled
|
|
58
|
+
* `require()`; if the peer turns out to be ESM-only, the load is retried
|
|
59
|
+
* through the ESM-build sibling copy of this file, whose `import()` survived
|
|
60
|
+
* transpilation (see the module doc comment).
|
|
61
|
+
*
|
|
62
|
+
* @param specifier bare package specifier (e.g. `'@zvndev/powdb-client'`).
|
|
63
|
+
* @param allowEsmFallback internal recursion guard — the delegated call passes
|
|
64
|
+
* `false` so a failure in the sibling copy can never bounce back.
|
|
65
|
+
*/
|
|
66
|
+
async function importOptionalPeer(specifier, allowEsmFallback = true) {
|
|
67
|
+
try {
|
|
68
|
+
return await import(specifier);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (!allowEsmFallback || !isEsmOnlyLoadError(err))
|
|
72
|
+
throw err;
|
|
73
|
+
let esmCapableCopy;
|
|
74
|
+
try {
|
|
75
|
+
// Only resolvable from dist/cjs/, where it lands on the NodeNext-built
|
|
76
|
+
// dist/optional-peer-import.cjs. From anywhere else (the ESM copy
|
|
77
|
+
// itself, or running the TypeScript source directly) the file does not
|
|
78
|
+
// exist and the original error is rethrown below.
|
|
79
|
+
esmCapableCopy = require('../optional-peer-import.cjs');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
esmCapableCopy = undefined;
|
|
83
|
+
}
|
|
84
|
+
if (typeof esmCapableCopy !== 'function')
|
|
85
|
+
throw err;
|
|
86
|
+
return esmCapableCopy(specifier, false);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
module.exports = importOptionalPeer;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True dynamic `import()` for the optional peer dependencies (`mysql2`,
|
|
3
|
+
* `mssql`, `@zvndev/powdb-client`, `@zvndev/powdb-embedded`) — safe in BOTH
|
|
4
|
+
* build outputs, including for peers that are ESM-only.
|
|
5
|
+
*
|
|
6
|
+
* THE PROBLEM THIS FILE SOLVES (the `@zvndev/powdb-client` ≥ 0.9 CJS break):
|
|
7
|
+
* the engine subpaths load their optional peers with dynamic `import()` so the
|
|
8
|
+
* peers stay out of the static graph. The ESM build (`tsconfig.json`, module
|
|
9
|
+
* NodeNext) emits that `import()` verbatim. The CJS build (`tsconfig.cjs.json`,
|
|
10
|
+
* module CommonJS) however TRANSPILES `import()` into
|
|
11
|
+
* `Promise.resolve().then(() => require(...))` — and `require()` of an
|
|
12
|
+
* ESM-only package (no `require` export condition, e.g. powdb-client ≥ 0.9)
|
|
13
|
+
* throws `ERR_PACKAGE_PATH_NOT_EXPORTED`, breaking every CJS consumer.
|
|
14
|
+
*
|
|
15
|
+
* TypeScript offers no way to preserve `import()` under `module: CommonJS`,
|
|
16
|
+
* and the CJS pass cannot switch to `module: NodeNext` (the root package.json
|
|
17
|
+
* says `"type": "module"`, so NodeNext would classify every `.ts` source as
|
|
18
|
+
* ESM and emit ESM into dist/cjs). A `.cts` file is the escape hatch: it is
|
|
19
|
+
* CommonJS-format by extension regardless of package `type`, so under the ESM
|
|
20
|
+
* pass (NodeNext) it compiles to `dist/optional-peer-import.cjs` — a CommonJS
|
|
21
|
+
* file whose `import()` SURVIVES transpilation (NodeNext preserves dynamic
|
|
22
|
+
* import in CJS files precisely because it is the only way CJS can load ESM).
|
|
23
|
+
*
|
|
24
|
+
* That gives the published package two copies of this module:
|
|
25
|
+
* - `dist/optional-peer-import.cjs` (ESM pass, NodeNext) — real `import()`
|
|
26
|
+
* - `dist/cjs/optional-peer-import.cjs` (CJS pass, CommonJS) — lowered to `require()`
|
|
27
|
+
*
|
|
28
|
+
* The lowered copy works fine for CJS-loadable peers (`mysql2`, `mssql`, older
|
|
29
|
+
* powdb peers). When it hits an ESM-only peer, the `require()` fails with a
|
|
30
|
+
* recognizable code and this function falls back to delegating the load to the
|
|
31
|
+
* sibling NodeNext copy one directory up (`../optional-peer-import.cjs`) —
|
|
32
|
+
* which is a plain CommonJS file (loadable by `require()` on every supported
|
|
33
|
+
* Node) whose real `import()` then loads the ESM peer. The ESM-pass copy has
|
|
34
|
+
* no such sibling; its lazy `require` throws and the original error surfaces,
|
|
35
|
+
* so the fallback can never recurse.
|
|
36
|
+
*
|
|
37
|
+
* Keep this module dependency-free and side-effect-free: it must be loadable
|
|
38
|
+
* from both module systems on every supported Node (≥ 20) without pulling in
|
|
39
|
+
* anything else.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Dynamically import an optional peer dependency. In the ESM build this is a
|
|
43
|
+
* plain `import()`. In the CJS build the first attempt is a transpiled
|
|
44
|
+
* `require()`; if the peer turns out to be ESM-only, the load is retried
|
|
45
|
+
* through the ESM-build sibling copy of this file, whose `import()` survived
|
|
46
|
+
* transpilation (see the module doc comment).
|
|
47
|
+
*
|
|
48
|
+
* @param specifier bare package specifier (e.g. `'@zvndev/powdb-client'`).
|
|
49
|
+
* @param allowEsmFallback internal recursion guard — the delegated call passes
|
|
50
|
+
* `false` so a failure in the sibling copy can never bounce back.
|
|
51
|
+
*/
|
|
52
|
+
declare function importOptionalPeer(specifier: string, allowEsmFallback?: boolean): Promise<unknown>;
|
|
53
|
+
export = importOptionalPeer;
|
package/dist/powdb.d.ts
CHANGED
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
* impossible → it degrades to batched N+1 loaders (Phase B).
|
|
25
25
|
* - **Single global write lock; no savepoints/isolation** — nested
|
|
26
26
|
* transactions / isolation / vector / LISTEN-NOTIFY / RLS throw.
|
|
27
|
+
* Independent concurrent `db.$transaction` calls do NOT throw: they queue
|
|
28
|
+
* FIFO on a pool-level gate and run one at a time (see {@link PowdbTxGate}).
|
|
29
|
+
* Only a *re-entrant* transaction — a `db.$transaction` opened from inside
|
|
30
|
+
* an active transaction callback's async context, which queueing would
|
|
31
|
+
* deadlock — fails fast with E017.
|
|
27
32
|
* - **The wire protocol pipelines** — `@zvndev/powdb-client` writes each
|
|
28
33
|
* request frame immediately and matches replies FIFO, so multiple queries
|
|
29
34
|
* may be in flight on one connection. {@link PowdbPool}'s checked-out
|
|
@@ -65,9 +70,11 @@ import type { ColumnMetadata, SchemaMetadata, TableMetadata } from './schema.js'
|
|
|
65
70
|
* {@link UnsupportedFeatureError} (E017): a nested `tx.$transaction` emits a
|
|
66
71
|
* savepoint synchronously (before any DB call) and so fails fast with a
|
|
67
72
|
* clear typed error instead of leaking PowDB's cryptic `Parse(... 'sp_1')`.
|
|
68
|
-
* The pool-level
|
|
69
|
-
* other
|
|
70
|
-
*
|
|
73
|
+
* The pool-level transaction gate (see {@link PowdbTxGate}) handles the
|
|
74
|
+
* other shapes: a fresh top-level `db.$transaction` opened inside an
|
|
75
|
+
* already-open one throws E017 before it can deadlock on the write lock,
|
|
76
|
+
* while INDEPENDENT concurrent `db.$transaction` calls queue FIFO and run
|
|
77
|
+
* one at a time instead of failing.
|
|
71
78
|
* Isolation levels remain Phase B.
|
|
72
79
|
*/
|
|
73
80
|
export declare const powdbDialect: Dialect;
|
|
@@ -113,6 +120,18 @@ interface PowdbClientPool {
|
|
|
113
120
|
withClient<T>(fn: (c: PowdbClient) => Promise<T>): Promise<T>;
|
|
114
121
|
close(): Promise<void>;
|
|
115
122
|
}
|
|
123
|
+
interface PowdbModule {
|
|
124
|
+
Client: {
|
|
125
|
+
connect(opts: PowdbConnOptions): Promise<PowdbClient>;
|
|
126
|
+
};
|
|
127
|
+
Pool: new (opts: PowdbConnOptions & {
|
|
128
|
+
max?: number;
|
|
129
|
+
}) => PowdbClientPool;
|
|
130
|
+
isPowDBError?(err: unknown): err is {
|
|
131
|
+
code: string;
|
|
132
|
+
message: string;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
116
135
|
/** Connection options for {@link turbinePowDB} — host/port, not a connection string. */
|
|
117
136
|
export interface PowdbConnOptions {
|
|
118
137
|
host: string;
|
|
@@ -161,6 +180,24 @@ export declare function powqlColumnType(col: ColumnMetadata): PowqlType;
|
|
|
161
180
|
* `auto` modifier, so PowDB assigns a monotonic id on insert and Turbine stops
|
|
162
181
|
* synthesizing a client-side value for it.
|
|
163
182
|
*/
|
|
183
|
+
/**
|
|
184
|
+
* PowQL reserved words — the v0.10 lexer keyword table from POWQL.md's
|
|
185
|
+
* "Reserved Words and Quoting" section, including the v0.10 additions
|
|
186
|
+
* `schema` and `describe`. Keyword matching is case-sensitive in the lexer,
|
|
187
|
+
* so only the exact lowercase form collides.
|
|
188
|
+
*/
|
|
189
|
+
export declare const POWQL_KEYWORDS: ReadonlySet<string>;
|
|
190
|
+
/**
|
|
191
|
+
* Backtick-quote an identifier when PowQL would otherwise lex it as a keyword
|
|
192
|
+
* (or when it contains characters outside the bare-identifier grammar).
|
|
193
|
+
* Applied only in bare-identifier positions — DDL type/field names, index DDL,
|
|
194
|
+
* and `insert`/`update`/`upsert` assignment targets. Dotted references
|
|
195
|
+
* (`.col` in filters/projections/ordering) bypass keyword lookup on every
|
|
196
|
+
* engine version and deliberately stay bare for ≤0.9 compatibility. Backticks
|
|
197
|
+
* parse on PowDB ≥ 0.10; on older engines these names were already parse
|
|
198
|
+
* errors when emitted bare, so quoting is strictly an improvement.
|
|
199
|
+
*/
|
|
200
|
+
export declare function quotePowqlIdent(name: string): string;
|
|
164
201
|
export declare function powqlSchemaDDL(schema: SchemaMetadata): string[];
|
|
165
202
|
/**
|
|
166
203
|
* Coerce a single PowDB wire string into the JS value its column type implies.
|
|
@@ -194,6 +231,27 @@ type QueryArg = string | {
|
|
|
194
231
|
text: string;
|
|
195
232
|
values?: unknown[];
|
|
196
233
|
};
|
|
234
|
+
/**
|
|
235
|
+
* Default cap (ms) on how long a `begin` may wait in the FIFO queue for
|
|
236
|
+
* PowDB's single global write lock before failing with a typed
|
|
237
|
+
* {@link TimeoutError} (E002). Prevents silent starvation behind a wedged
|
|
238
|
+
* transaction. Override via `transactionQueueTimeoutMs`
|
|
239
|
+
* ({@link TurbinePowdbOptions} / {@link PowdbPoolOptions}); `0` or `Infinity`
|
|
240
|
+
* waits without limit.
|
|
241
|
+
*/
|
|
242
|
+
export declare const DEFAULT_TX_QUEUE_TIMEOUT_MS = 30000;
|
|
243
|
+
/** Tuning options shared by {@link PowdbPool} and {@link PowdbEmbeddedPool}. */
|
|
244
|
+
export interface PowdbPoolOptions {
|
|
245
|
+
/**
|
|
246
|
+
* Max time (ms) a concurrent transaction's `begin` may wait in the FIFO
|
|
247
|
+
* queue for the single-writer lock before failing with a
|
|
248
|
+
* {@link TimeoutError}. Default {@link DEFAULT_TX_QUEUE_TIMEOUT_MS};
|
|
249
|
+
* `0` / `Infinity` = wait without limit. Note this is a separate surface
|
|
250
|
+
* from `$transaction`'s `timeout` option, which only covers the callback
|
|
251
|
+
* *after* the transaction has begun.
|
|
252
|
+
*/
|
|
253
|
+
transactionQueueTimeoutMs?: number;
|
|
254
|
+
}
|
|
197
255
|
/**
|
|
198
256
|
* A {@link PgCompatPool} backed by a `@zvndev/powdb-client` `Pool`. The query
|
|
199
257
|
* `text` is **PowQL**, not SQL — {@link PowqlInterface} generates it. Rows come
|
|
@@ -205,21 +263,31 @@ export declare class PowdbPool implements PgCompatPool {
|
|
|
205
263
|
private readonly toParam;
|
|
206
264
|
private closed;
|
|
207
265
|
/**
|
|
208
|
-
* Pool-level single-writer
|
|
209
|
-
* most one transaction may be open across the whole pool.
|
|
210
|
-
*
|
|
211
|
-
* connection and
|
|
266
|
+
* Pool-level single-writer gate. PowDB holds one global write lock, so at
|
|
267
|
+
* most one transaction may be open across the whole pool. Concurrent
|
|
268
|
+
* `begin`s queue FIFO on the gate (instead of checking out a second
|
|
269
|
+
* connection and blocking on the lock forever — the networked hang);
|
|
270
|
+
* re-entrant `begin`s throw E017 (see {@link PowdbTxGate}).
|
|
212
271
|
*/
|
|
213
|
-
private
|
|
214
|
-
|
|
272
|
+
private readonly txGate;
|
|
273
|
+
/** Hold taken by a `begin` issued via `query()` directly (no checked-out client). */
|
|
274
|
+
private poolHold;
|
|
215
275
|
/**
|
|
216
|
-
*
|
|
217
|
-
* (
|
|
218
|
-
*
|
|
219
|
-
*
|
|
276
|
+
* Clients currently checked out via {@link connect}. The driver pool's
|
|
277
|
+
* `close()` only closes IDLE clients (checked-out ones are documented as the
|
|
278
|
+
* caller's responsibility), so {@link end} destroys these explicitly;
|
|
279
|
+
* otherwise a `disconnect()` racing an unreleased connection would leave a
|
|
280
|
+
* live socket holding the process open until the server's idle timeout.
|
|
220
281
|
*/
|
|
221
|
-
private
|
|
282
|
+
private readonly checkedOut;
|
|
283
|
+
constructor(pool: PowdbClientPool, toParam?: (v: unknown, i: number) => PowdbParam, options?: PowdbPoolOptions);
|
|
222
284
|
query(text: QueryArg, values?: unknown[]): Promise<any>;
|
|
285
|
+
/**
|
|
286
|
+
* Typed guard mirroring {@link PowdbEmbeddedPool}: after `end()` the driver
|
|
287
|
+
* pool throws a raw `Error('pool closed')` that {@link wrapPowdbError}
|
|
288
|
+
* cannot classify — surface the same ConnectionError on both transports.
|
|
289
|
+
*/
|
|
290
|
+
private assertOpen;
|
|
223
291
|
connect(): Promise<PgCompatPoolClient>;
|
|
224
292
|
end(): Promise<void>;
|
|
225
293
|
}
|
|
@@ -278,17 +346,27 @@ export declare class PowdbEmbeddedPool implements PgCompatPool {
|
|
|
278
346
|
private readonly db;
|
|
279
347
|
private closed;
|
|
280
348
|
/**
|
|
281
|
-
* Single-writer
|
|
349
|
+
* Single-writer gate. The embedded engine is one handle with one global
|
|
282
350
|
* write lock — only one transaction may be open at a time. A re-entrant
|
|
283
|
-
* `begin` (a fresh top-level `db.$transaction` opened inside an open one
|
|
284
|
-
* would otherwise hit PowDB's raw "already in a transaction"
|
|
285
|
-
*
|
|
286
|
-
*
|
|
351
|
+
* `begin` (a fresh top-level `db.$transaction` opened inside an open one's
|
|
352
|
+
* callback) would otherwise hit PowDB's raw "already in a transaction"
|
|
353
|
+
* parse error; the gate surfaces a typed E017 instead, while INDEPENDENT
|
|
354
|
+
* concurrent transactions queue FIFO and run one at a time. (Nested
|
|
355
|
+
* `tx.$transaction` is caught earlier still, by the savepoint override in
|
|
356
|
+
* {@link powdbDialect}.)
|
|
357
|
+
*/
|
|
358
|
+
private readonly txGate;
|
|
359
|
+
/** Hold taken by a `begin` issued via `query()` directly (no checked-out client). */
|
|
360
|
+
private readonly poolHoldRef;
|
|
361
|
+
constructor(db: EmbeddedDatabase, options?: PowdbPoolOptions);
|
|
362
|
+
/** Materialize `$N` params and hand the PowQL to the in-process engine. */
|
|
363
|
+
private exec;
|
|
364
|
+
/**
|
|
365
|
+
* Run one statement, gating transaction control. `holdRef` scopes the gate
|
|
366
|
+
* hold to whoever issued the `begin` (the pool itself or one checked-out
|
|
367
|
+
* client), so finishing a transaction can never release a slot a different
|
|
368
|
+
* transaction is holding.
|
|
287
369
|
*/
|
|
288
|
-
private activeTransaction;
|
|
289
|
-
constructor(db: EmbeddedDatabase);
|
|
290
|
-
/** Enforce the single-writer model on a transaction-control statement. */
|
|
291
|
-
private guardTxControl;
|
|
292
370
|
private run;
|
|
293
371
|
query(text: QueryArg, values?: unknown[]): Promise<any>;
|
|
294
372
|
connect(): Promise<PgCompatPoolClient>;
|
|
@@ -299,6 +377,23 @@ export { PowqlInterface } from './powql.js';
|
|
|
299
377
|
export interface TurbinePowdbOptions extends Pick<TurbineConfig, 'logging' | 'defaultLimit' | 'warnOnUnlimited'> {
|
|
300
378
|
/** Max pooled connections (default 10). Networked transport only. */
|
|
301
379
|
connectionLimit?: number;
|
|
380
|
+
/**
|
|
381
|
+
* Max time (ms) a concurrent `$transaction` waits in the FIFO queue for
|
|
382
|
+
* PowDB's single global write lock before failing with a typed
|
|
383
|
+
* `TimeoutError` (default {@link DEFAULT_TX_QUEUE_TIMEOUT_MS} = 30 000;
|
|
384
|
+
* `0` / `Infinity` = wait without limit). Independent concurrent
|
|
385
|
+
* transactions queue and run one at a time; only a re-entrant
|
|
386
|
+
* `db.$transaction` (opened inside an active transaction callback) throws
|
|
387
|
+
* E017 — queueing that shape would deadlock.
|
|
388
|
+
*/
|
|
389
|
+
transactionQueueTimeoutMs?: number;
|
|
390
|
+
/**
|
|
391
|
+
* Driver-module injection for the networked target forms (URL / host+port):
|
|
392
|
+
* bypasses the dynamic `import('@zvndev/powdb-client')` and uses this object
|
|
393
|
+
* as the driver module instead. Intended for tests (a fake pool that counts
|
|
394
|
+
* connections) and advanced embedding; everyday callers never set it.
|
|
395
|
+
*/
|
|
396
|
+
powdbClientModule?: PowdbModule;
|
|
302
397
|
}
|
|
303
398
|
/**
|
|
304
399
|
* Selects the **embedded** transport — an in-process `@zvndev/powdb-embedded`
|