turbine-orm 0.32.2 → 0.34.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 +2 -2
- package/dist/cjs/dialect.js +1 -0
- package/dist/cjs/index-advisor.js +0 -0
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/mssql.js +3 -0
- package/dist/cjs/mysql.js +3 -0
- package/dist/cjs/optional-peer-import.cjs +28 -0
- package/dist/cjs/powdb-introspect.js +222 -0
- package/dist/cjs/powdb.js +446 -55
- package/dist/cjs/powql.js +566 -111
- package/dist/cjs/query/builder.js +136 -53
- package/dist/cjs/query/filters.js +4 -4
- package/dist/cjs/schema-builder.js +16 -0
- package/dist/cjs/schema-metadata.js +81 -10
- package/dist/cjs/sqlite.js +2 -0
- package/dist/dialect.d.ts +7 -0
- package/dist/dialect.js +1 -0
- package/dist/index-advisor.d.ts +15 -1
- package/dist/index-advisor.js +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mssql.js +3 -0
- package/dist/mysql.js +3 -0
- package/dist/optional-peer-import.cjs +28 -0
- package/dist/optional-peer-import.d.cts +19 -0
- package/dist/powdb-introspect.d.ts +84 -0
- package/dist/powdb-introspect.js +219 -0
- package/dist/powdb.d.ts +249 -13
- package/dist/powdb.js +438 -54
- package/dist/powql.d.ts +113 -6
- package/dist/powql.js +568 -113
- package/dist/query/builder.d.ts +11 -0
- package/dist/query/builder.js +136 -53
- package/dist/query/filters.d.ts +3 -3
- package/dist/query/filters.js +4 -4
- package/dist/query/types.d.ts +50 -6
- package/dist/schema-builder.d.ts +46 -1
- package/dist/schema-builder.js +15 -0
- package/dist/schema-metadata.d.ts +13 -7
- package/dist/schema-metadata.js +82 -11
- package/dist/schema.d.ts +25 -0
- package/dist/sqlite.js +2 -0
- package/package.json +3 -3
|
@@ -995,14 +995,22 @@ class QueryInterface {
|
|
|
995
995
|
else {
|
|
996
996
|
selectClause = `${qt}.*`;
|
|
997
997
|
}
|
|
998
|
-
|
|
998
|
+
// Piece-then-assemble. The join-sink between FROM and WHERE carries any
|
|
999
|
+
// `plan: 'lateral'` pick joins; it is populated during the ORDER BY build
|
|
1000
|
+
// below and spliced in at final assembly. Empty for every other query
|
|
1001
|
+
// shape → the assembled SQL is byte-identical to the incremental-append
|
|
1002
|
+
// form for the default plan (asserted by the byte-equality snapshot test).
|
|
1003
|
+
const lateralJoins = [];
|
|
1004
|
+
// WHERE + cursor conditions accumulate into `tail`, pushing params in
|
|
1005
|
+
// where → cursor order (the collect path mirrors this exactly).
|
|
1006
|
+
let tail = freshWhereSql;
|
|
999
1007
|
if (args?.cursor) {
|
|
1000
1008
|
// Sorted (canonical) order — MUST match cursorFp and the cache-hit collect below.
|
|
1001
1009
|
const cursorEntries = (0, filters_js_1.sortedEntries)(args.cursor).filter(([, v]) => v !== undefined);
|
|
1002
1010
|
if (cursorEntries.length > 0) {
|
|
1003
1011
|
const cursorConditions = cursorEntries.map(([k, v]) => {
|
|
1004
1012
|
const col = this.toSqlColumn(k);
|
|
1005
|
-
// orderBy values can be the { sort, nulls } spec form
|
|
1013
|
+
// orderBy values can be the { sort, nulls } spec form: normalize
|
|
1006
1014
|
// before comparing, or a desc spec would seek the ascending side.
|
|
1007
1015
|
const dir = args.orderBy?.[k];
|
|
1008
1016
|
const desc = (0, filters_js_1.isOrderBySpec)(dir) ? dir.sort === 'desc' : dir === 'desc';
|
|
@@ -1010,33 +1018,38 @@ class QueryInterface {
|
|
|
1010
1018
|
freshParams.push(v);
|
|
1011
1019
|
return `${qt}.${col} ${op} ${this.p(freshParams.length)}`;
|
|
1012
1020
|
});
|
|
1013
|
-
|
|
1014
|
-
sql += ` AND ${cursorConditions.join(' AND ')}`;
|
|
1015
|
-
}
|
|
1016
|
-
else {
|
|
1017
|
-
sql += ` WHERE ${cursorConditions.join(' AND ')}`;
|
|
1018
|
-
}
|
|
1021
|
+
tail += freshWhereSql ? ` AND ${cursorConditions.join(' AND ')}` : ` WHERE ${cursorConditions.join(' AND ')}`;
|
|
1019
1022
|
}
|
|
1020
1023
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
else {
|
|
1036
|
-
// Pass freshParams so vector KNN ordering binds its `$n::vector` query
|
|
1037
|
-
// vector at the correct position (after cursor params, before LIMIT).
|
|
1038
|
-
sql += ` ORDER BY ${this.buildOrderBy(args.orderBy, freshParams)}`;
|
|
1024
|
+
// ORDER BY is built AFTER the cursor pushes (param order
|
|
1025
|
+
// where → with → cursor → orderBy → limit → offset) and BEFORE final
|
|
1026
|
+
// assembly (so the lateral sink is filled before the FROM clause is
|
|
1027
|
+
// written). distinct + relation orderBy is refused up front, so a lateral
|
|
1028
|
+
// pick can never reach the distinct branch (lateralJoins stays empty).
|
|
1029
|
+
let sql;
|
|
1030
|
+
if (args?.orderBy && distinctPrefix) {
|
|
1031
|
+
// Postgres requires DISTINCT ON expressions to lead the ORDER BY. Prisma
|
|
1032
|
+
// semantics ("first row per combination, result in the user's order")
|
|
1033
|
+
// need two levels: inner DISTINCT ON ordered by the distinct columns then
|
|
1034
|
+
// the user's order (picks the right representative row), outer re-ordered
|
|
1035
|
+
// by the user's order alone.
|
|
1036
|
+
if (Object.values(args.orderBy).some((d) => (0, filters_js_1.isVectorOrderBy)(d))) {
|
|
1037
|
+
throw new errors_js_1.ValidationError('[turbine] `distinct` cannot be combined with vector distance ordering.');
|
|
1039
1038
|
}
|
|
1039
|
+
const userOrder = this.buildOrderBy(args.orderBy, freshParams);
|
|
1040
|
+
const inner = `SELECT ${distinctPrefix}${selectClause} FROM ${qt}${tail} ORDER BY ${distinctCols
|
|
1041
|
+
.map((c) => `${c} ASC`)
|
|
1042
|
+
.join(', ')}, ${userOrder}`;
|
|
1043
|
+
sql = `SELECT * FROM (${inner}) AS ${this.q(`${this.table}_distinct`)} ORDER BY ${userOrder}`;
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
// Pass freshParams so vector KNN ordering binds its `$n::vector` query
|
|
1047
|
+
// vector at the correct position (after cursor params, before LIMIT), and
|
|
1048
|
+
// lateralJoins so a lateral pick splices its join into the FROM clause.
|
|
1049
|
+
const orderBySql = args?.orderBy
|
|
1050
|
+
? ` ORDER BY ${this.buildOrderBy(args.orderBy, freshParams, lateralJoins)}`
|
|
1051
|
+
: '';
|
|
1052
|
+
sql = `SELECT ${distinctPrefix}${selectClause} FROM ${qt}${lateralJoins.join('')}${tail}${orderBySql}`;
|
|
1040
1053
|
}
|
|
1041
1054
|
// Pagination — push params in the same order the collect path mirrors
|
|
1042
1055
|
// (limit before offset); the SQL TEXT shape is dialect-owned via
|
|
@@ -4354,7 +4367,13 @@ class QueryInterface {
|
|
|
4354
4367
|
const pickWhere = d.pick?.where
|
|
4355
4368
|
? `;pw=${this.fingerprintAliasWhere(d.pick.where, targetTable)}`
|
|
4356
4369
|
: '';
|
|
4357
|
-
|
|
4370
|
+
// Plan discriminator: the lateral plan emits DIFFERENT SQL (a FROM-clause
|
|
4371
|
+
// join + a qualified order term) so a warm cache must never serve one
|
|
4372
|
+
// plan's SQL for the other. Emitted ONLY for `'lateral'`: absent means
|
|
4373
|
+
// the default subquery plan, keeping every pre-existing cache key
|
|
4374
|
+
// byte-identical (no cold-cache churn on upgrade).
|
|
4375
|
+
const planTag = d.plan === 'lateral' ? ';plan=lat' : '';
|
|
4376
|
+
return `pick(${by},${d.direction ?? 'asc'},${d.nulls ?? ''};po=${pickOrder}${pickWhere}${planTag})`;
|
|
4358
4377
|
}
|
|
4359
4378
|
if ((0, filters_js_1.isOrderBySpec)(d))
|
|
4360
4379
|
return `spec(${d.sort},${d.nulls ?? ''})`;
|
|
@@ -4371,7 +4390,7 @@ class QueryInterface {
|
|
|
4371
4390
|
}
|
|
4372
4391
|
return String(d);
|
|
4373
4392
|
}
|
|
4374
|
-
buildOrderBy(orderBy, params) {
|
|
4393
|
+
buildOrderBy(orderBy, params, lateralSink) {
|
|
4375
4394
|
// Dev-only: validate that orderBy fields exist in the table schema. Relation
|
|
4376
4395
|
// orderBy keys (object values that are neither a vector nor an OrderBySpec)
|
|
4377
4396
|
// are validated in the relation branch below, so skip them here.
|
|
@@ -4414,7 +4433,7 @@ class QueryInterface {
|
|
|
4414
4433
|
// keyed by a relation name (`{ posts: { _count: 'desc' } }` / `{ author:
|
|
4415
4434
|
// { name: 'asc' } }`).
|
|
4416
4435
|
if (this.isRelationOrderByValue(value)) {
|
|
4417
|
-
return this.buildRelationOrderBy(key, value, `ord${relOrdCounter++}`, params);
|
|
4436
|
+
return this.buildRelationOrderBy(key, value, `ord${relOrdCounter++}`, params, undefined, lateralSink);
|
|
4418
4437
|
}
|
|
4419
4438
|
// Scalar column ordering — a plain direction or an OrderBySpec (nulls).
|
|
4420
4439
|
if (meta && !(key in meta.columnMap)) {
|
|
@@ -4508,7 +4527,18 @@ class QueryInterface {
|
|
|
4508
4527
|
const extract = this.dialect.buildJsonPathExtract(`${prefix}${this.q(col)}`, this.p(params.length));
|
|
4509
4528
|
const lhs = spec.type === 'numeric' ? this.castJsonNumeric(extract) : extract;
|
|
4510
4529
|
const dir = spec.direction?.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
4511
|
-
|
|
4530
|
+
// Rows whose document lacks the path extract to NULL. Without a nulls
|
|
4531
|
+
// clause, Postgres DESC defaults to NULLS FIRST, which both diverges from
|
|
4532
|
+
// pick-row ordering (NULLS LAST both directions since 0.33) and from
|
|
4533
|
+
// engines whose path ordering is nulls-last in both directions. Default to
|
|
4534
|
+
// NULLS LAST in BOTH directions unless the caller set `nulls` explicitly;
|
|
4535
|
+
// the grammar gate matches nullsSuffix.
|
|
4536
|
+
const nullsSql = spec.nulls
|
|
4537
|
+
? this.nullsSuffix(spec.nulls)
|
|
4538
|
+
: this.dialect.name === 'postgresql' || this.dialect.name === 'sqlite'
|
|
4539
|
+
? ' NULLS LAST'
|
|
4540
|
+
: '';
|
|
4541
|
+
return `${lhs} ${dir}${nullsSql}`;
|
|
4512
4542
|
}
|
|
4513
4543
|
/**
|
|
4514
4544
|
* Compile a relation ordering term. For a to-many relation the only allowed
|
|
@@ -4523,7 +4553,7 @@ class QueryInterface {
|
|
|
4523
4553
|
* subquery's orderBy the relations live on the TARGET table's metadata and
|
|
4524
4554
|
* the correlation parent is the relation's alias, not `this.table`.
|
|
4525
4555
|
*/
|
|
4526
|
-
buildRelationOrderBy(relName, value, alias, params, ctx) {
|
|
4556
|
+
buildRelationOrderBy(relName, value, alias, params, ctx, lateralSink) {
|
|
4527
4557
|
const ownerMeta = ctx?.meta ?? this.tableMeta;
|
|
4528
4558
|
const ownerTable = ctx?.table ?? this.table;
|
|
4529
4559
|
const parentRef = ctx?.parentRef ?? this.table;
|
|
@@ -4539,7 +4569,7 @@ class QueryInterface {
|
|
|
4539
4569
|
// scope errors, shared with the cache-hit collect mirror.
|
|
4540
4570
|
if ((0, filters_js_1.isRelationPickOrderBy)(value)) {
|
|
4541
4571
|
this.validatePickOrderBy(relName, relDef, value, ctx !== undefined);
|
|
4542
|
-
return this.buildRelationPickOrderBy(relName, relDef, value, alias, parentRef, params);
|
|
4572
|
+
return this.buildRelationPickOrderBy(relName, relDef, value, alias, parentRef, params, lateralSink);
|
|
4543
4573
|
}
|
|
4544
4574
|
// To-many: only `_count` is meaningful → correlated COUNT(*) subquery.
|
|
4545
4575
|
if (relDef.type === 'hasMany' || relDef.type === 'manyToMany') {
|
|
@@ -4631,6 +4661,25 @@ class QueryInterface {
|
|
|
4631
4661
|
throw new errors_js_1.ValidationError(`[turbine] Pick-row ordering on relation "${relName}" requires \`by\`: a target column name ` +
|
|
4632
4662
|
"or a JSON-path spec ({ field: 'data', path: ['title'] }).");
|
|
4633
4663
|
}
|
|
4664
|
+
// Physical plan gate. A typo like `plan: 'latreal'` must never silently run
|
|
4665
|
+
// the subquery plan (a silent plan change wearing a validation gap). Shared
|
|
4666
|
+
// by build and cache-hit collect so a warmed cache throws identically.
|
|
4667
|
+
if (spec.plan !== undefined && spec.plan !== 'subquery' && spec.plan !== 'lateral') {
|
|
4668
|
+
throw new errors_js_1.ValidationError(`[turbine] Pick-row ordering on relation "${relName}" has an invalid \`plan\`: ` +
|
|
4669
|
+
`${JSON.stringify(spec.plan)}. Use 'subquery' (default) or 'lateral'.`);
|
|
4670
|
+
}
|
|
4671
|
+
if (spec.plan === 'lateral') {
|
|
4672
|
+
if (!this.dialect.supportsLateralJoin) {
|
|
4673
|
+
throw new errors_js_1.UnsupportedFeatureError("pick-row ordering with plan: 'lateral'", this.dialect.name, "LATERAL joins are only available on PostgreSQL. Omit `plan` (or use 'subquery').");
|
|
4674
|
+
}
|
|
4675
|
+
// The lateral exposes one reserved output column, `__turbine_pick`. A
|
|
4676
|
+
// parent column with that exact name would make the unqualified WHERE
|
|
4677
|
+
// reference ambiguous once the join is in scope; refuse it explicitly.
|
|
4678
|
+
if (this.tableMeta.allColumns.includes('__turbine_pick')) {
|
|
4679
|
+
throw new errors_js_1.ValidationError(`[turbine] Pick-row ordering with plan: 'lateral' cannot be used: table "${this.tableMeta.name}" ` +
|
|
4680
|
+
'has a column named "__turbine_pick", which the lateral join output reserves.');
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4634
4683
|
}
|
|
4635
4684
|
/**
|
|
4636
4685
|
* Compile a {@link RelationPickOrderBy} term: a correlated scalar subquery
|
|
@@ -4649,36 +4698,83 @@ class QueryInterface {
|
|
|
4649
4698
|
* {@link collectRelationPickOrderParams}): `by` JSON path (if any) →
|
|
4650
4699
|
* target global filter → `pick.where` → `pick.orderBy` JSON paths.
|
|
4651
4700
|
*/
|
|
4652
|
-
buildRelationPickOrderBy(relName, relDef, spec, alias, parentRef, params) {
|
|
4701
|
+
buildRelationPickOrderBy(relName, relDef, spec, alias, parentRef, params, lateralSink) {
|
|
4653
4702
|
if (!params) {
|
|
4654
4703
|
throw new errors_js_1.ValidationError(`[turbine] Pick-row ordering on relation "${relName}" is only supported in a top-level findMany orderBy.`);
|
|
4655
4704
|
}
|
|
4656
4705
|
const targetMeta = this.schema.tables[relDef.to];
|
|
4657
4706
|
if (!targetMeta)
|
|
4658
4707
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation target "${relDef.to}"`);
|
|
4708
|
+
const dir = spec.direction?.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
4709
|
+
const limitOne = this.buildPagination('1', undefined, true);
|
|
4710
|
+
// Parents with ZERO surviving related rows have no row to pick: the
|
|
4711
|
+
// correlated subquery yields NULL, and the LEFT JOIN LATERAL null-extends
|
|
4712
|
+
// its single row identically. Without a nulls clause, Postgres DESC
|
|
4713
|
+
// defaults to NULLS FIRST (every childless parent tops a "highest first"
|
|
4714
|
+
// sort). Default to NULLS LAST in BOTH directions (deterministic across
|
|
4715
|
+
// engines: SQLite's NULL-is-smallest default diverges from Postgres) unless
|
|
4716
|
+
// the caller set `nulls` explicitly; the grammar gate matches nullsSuffix.
|
|
4717
|
+
const nullsSql = spec.nulls
|
|
4718
|
+
? this.nullsSuffix(spec.nulls)
|
|
4719
|
+
: this.dialect.name === 'postgresql' || this.dialect.name === 'sqlite'
|
|
4720
|
+
? ' NULLS LAST'
|
|
4721
|
+
: '';
|
|
4722
|
+
// Lateral plan: splice a `LEFT JOIN LATERAL (... LIMIT 1) ON true` into the
|
|
4723
|
+
// FROM clause (via the sink) and order by its single reserved output column.
|
|
4724
|
+
// Param push order is IDENTICAL to the subquery plan (compilePickPieces is
|
|
4725
|
+
// shared), so the cache-hit collect mirror needs no changes. Scope +
|
|
4726
|
+
// capability were already enforced by validatePickOrderBy (shared with the
|
|
4727
|
+
// collect path); the missing-sink guard catches a non-findMany build
|
|
4728
|
+
// context and hard-fails rather than silently emitting a subquery.
|
|
4729
|
+
if (spec.plan === 'lateral') {
|
|
4730
|
+
if (!lateralSink) {
|
|
4731
|
+
throw new errors_js_1.ValidationError(`[turbine] Pick-row ordering with plan: 'lateral' on relation "${relName}" is only supported ` +
|
|
4732
|
+
'in a top-level findMany orderBy.');
|
|
4733
|
+
}
|
|
4734
|
+
const childAlias = `${alias}i`;
|
|
4735
|
+
const { byExpr, where, orderClause } = this.compilePickPieces(relDef, targetMeta, spec, childAlias, parentRef, params);
|
|
4736
|
+
lateralSink.push(` LEFT JOIN LATERAL (SELECT ${byExpr} AS ${this.q('__turbine_pick')} FROM ${this.q(relDef.to)} ${childAlias}` +
|
|
4737
|
+
` WHERE ${where}${orderClause}${limitOne}) ${alias} ON true`);
|
|
4738
|
+
return `${alias}.${this.q('__turbine_pick')} ${dir}${nullsSql}`;
|
|
4739
|
+
}
|
|
4740
|
+
// Subquery plan (default): a correlated scalar subquery in ORDER BY.
|
|
4741
|
+
const { byExpr, where, orderClause } = this.compilePickPieces(relDef, targetMeta, spec, alias, parentRef, params);
|
|
4742
|
+
return `(SELECT ${byExpr} FROM ${this.q(relDef.to)} ${alias} WHERE ${where}${orderClause}${limitOne}) ${dir}${nullsSql}`;
|
|
4743
|
+
}
|
|
4744
|
+
/**
|
|
4745
|
+
* Compile the shared inner pieces of a pick-row ordering against `childAlias`
|
|
4746
|
+
* (the table alias the related row is read from): the `by` value expression,
|
|
4747
|
+
* the correlation + target global filter + `pick.where` predicate, and the
|
|
4748
|
+
* `pick.orderBy` clause. Factored out of {@link buildRelationPickOrderBy} so
|
|
4749
|
+
* the subquery and lateral plans build IDENTICAL pieces in the SAME param
|
|
4750
|
+
* push order (`by` JSON path → target global filter → `pick.where` →
|
|
4751
|
+
* `pick.orderBy` JSON paths), which is why the collect mirror
|
|
4752
|
+
* ({@link collectRelationPickOrderParams}) is plan-agnostic.
|
|
4753
|
+
*/
|
|
4754
|
+
compilePickPieces(relDef, targetMeta, spec, childAlias, parentRef, params) {
|
|
4659
4755
|
// The value surfaced from the picked row (SELECT list: its param binds first).
|
|
4660
4756
|
let byExpr;
|
|
4661
4757
|
if (typeof spec.by === 'string') {
|
|
4662
4758
|
const col = this.resolveOrderByColumn(relDef.to, targetMeta, spec.by);
|
|
4663
|
-
byExpr = `${
|
|
4759
|
+
byExpr = `${childAlias}.${this.q(col)}`;
|
|
4664
4760
|
}
|
|
4665
4761
|
else {
|
|
4666
4762
|
const col = this.validateJsonPathOrderBy(relDef.to, targetMeta, spec.by.field, {
|
|
4667
4763
|
path: spec.by.path,
|
|
4668
4764
|
});
|
|
4669
4765
|
params.push(this.jsonPathParam(spec.by.path));
|
|
4670
|
-
const extract = this.dialect.buildJsonPathExtract(`${
|
|
4766
|
+
const extract = this.dialect.buildJsonPathExtract(`${childAlias}.${this.q(col)}`, this.p(params.length));
|
|
4671
4767
|
byExpr = spec.by.type === 'numeric' ? this.castJsonNumeric(extract) : extract;
|
|
4672
4768
|
}
|
|
4673
4769
|
// Correlation to the parent row, then the target's global filter (a
|
|
4674
4770
|
// soft-deleted / other-tenant row must never be picked: matches the
|
|
4675
4771
|
// `with` subquery and to-one relation-orderBy semantics), then pick.where.
|
|
4676
|
-
let where = this.dialect.buildCorrelation(
|
|
4677
|
-
const gf = this.targetGlobalFilterAlias(relDef.to,
|
|
4772
|
+
let where = this.dialect.buildCorrelation(childAlias, relDef.foreignKey, this.q(parentRef), relDef.referenceKey);
|
|
4773
|
+
const gf = this.targetGlobalFilterAlias(relDef.to, childAlias, params);
|
|
4678
4774
|
if (gf)
|
|
4679
4775
|
where += ` AND ${gf}`;
|
|
4680
4776
|
if (spec.pick.where) {
|
|
4681
|
-
const pickWhere = this.buildAliasWhere(relDef.to, targetMeta,
|
|
4777
|
+
const pickWhere = this.buildAliasWhere(relDef.to, targetMeta, childAlias, spec.pick.where, params);
|
|
4682
4778
|
if (pickWhere)
|
|
4683
4779
|
where += ` AND ${pickWhere}`;
|
|
4684
4780
|
}
|
|
@@ -4686,21 +4782,8 @@ class QueryInterface {
|
|
|
4686
4782
|
// (plain columns, OrderBySpec nulls, JSON-path specs); a nested pick in
|
|
4687
4783
|
// here routes back through buildRelationOrderBy with ctx set and throws
|
|
4688
4784
|
// the top-level-only E003.
|
|
4689
|
-
const orderClause = this.buildRelationOrderClause(relDef.to, targetMeta,
|
|
4690
|
-
|
|
4691
|
-
const limitOne = this.buildPagination('1', undefined, true);
|
|
4692
|
-
// Parents with ZERO surviving related rows make the correlated subquery
|
|
4693
|
-
// yield NULL. Without a nulls clause, Postgres DESC defaults to NULLS
|
|
4694
|
-
// FIRST — every childless parent would top a "highest first" sort. Default
|
|
4695
|
-
// to NULLS LAST in BOTH directions (deterministic across engines: SQLite's
|
|
4696
|
-
// NULL-is-smallest default diverges from Postgres) unless the caller set
|
|
4697
|
-
// `nulls` explicitly; the grammar gate matches nullsSuffix (PG + SQLite).
|
|
4698
|
-
const nullsSql = spec.nulls
|
|
4699
|
-
? this.nullsSuffix(spec.nulls)
|
|
4700
|
-
: this.dialect.name === 'postgresql' || this.dialect.name === 'sqlite'
|
|
4701
|
-
? ' NULLS LAST'
|
|
4702
|
-
: '';
|
|
4703
|
-
return `(SELECT ${byExpr} FROM ${this.q(relDef.to)} ${alias} WHERE ${where}${orderClause}${limitOne}) ${dir}${nullsSql}`;
|
|
4785
|
+
const orderClause = this.buildRelationOrderClause(relDef.to, targetMeta, childAlias, Object.entries(spec.pick.orderBy), params);
|
|
4786
|
+
return { byExpr, where, orderClause };
|
|
4704
4787
|
}
|
|
4705
4788
|
/**
|
|
4706
4789
|
* Param-collect mirror of {@link buildRelationPickOrderBy}: re-runs the same
|
|
@@ -319,9 +319,9 @@ function isJsonPathOrderBy(value) {
|
|
|
319
319
|
}
|
|
320
320
|
/**
|
|
321
321
|
* Check if an orderBy value is a pick-row relation ordering:
|
|
322
|
-
* `{ pick: { orderBy, ... }, by, direction?, nulls? }`. The full shape
|
|
323
|
-
* required
|
|
324
|
-
* present, and no keys outside `{ pick, by, direction, nulls }`
|
|
322
|
+
* `{ pick: { orderBy, ... }, by, direction?, nulls?, plan? }`. The full shape
|
|
323
|
+
* is required (`pick` must be an object carrying `orderBy`, `by` must be
|
|
324
|
+
* present, and no keys outside `{ pick, by, direction, nulls, plan }`), so a to-one
|
|
325
325
|
* relation whose target has real columns literally named `pick` and `by`
|
|
326
326
|
* (whose values are direction strings or `{ sort, nulls }` specs, never an
|
|
327
327
|
* object with `orderBy`) still falls through to column ordering. `distance`
|
|
@@ -339,7 +339,7 @@ function isRelationPickOrderBy(value) {
|
|
|
339
339
|
if (typeof v.pick !== 'object' || v.pick === null || Array.isArray(v.pick) || !('orderBy' in v.pick))
|
|
340
340
|
return false;
|
|
341
341
|
for (const key of Object.keys(v)) {
|
|
342
|
-
if (key !== 'pick' && key !== 'by' && key !== 'direction' && key !== 'nulls')
|
|
342
|
+
if (key !== 'pick' && key !== 'by' && key !== 'direction' && key !== 'nulls' && key !== 'plan')
|
|
343
343
|
return false;
|
|
344
344
|
}
|
|
345
345
|
return true;
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
27
|
exports.camelToSnake = exports.column = exports.ColumnBuilder = void 0;
|
|
28
|
+
exports.isDocFieldIndexDef = isDocFieldIndexDef;
|
|
28
29
|
exports.defineSchema = defineSchema;
|
|
29
30
|
exports.table = table;
|
|
30
31
|
exports.applyManyToManyRelations = applyManyToManyRelations;
|
|
@@ -102,6 +103,10 @@ function resolveColumn(def) {
|
|
|
102
103
|
check: def.check ?? null,
|
|
103
104
|
};
|
|
104
105
|
}
|
|
106
|
+
/** Type guard: is this index declaration a doc-field expression index? */
|
|
107
|
+
function isDocFieldIndexDef(idx) {
|
|
108
|
+
return 'docField' in idx && typeof idx.docField === 'string';
|
|
109
|
+
}
|
|
105
110
|
/** Check if a value is a TableDef (from legacy table() builder) */
|
|
106
111
|
function isTableDef(v) {
|
|
107
112
|
return typeof v === 'object' && v !== null && 'columns' in v && 'name' in v;
|
|
@@ -145,7 +150,17 @@ function defineSchema(input, options) {
|
|
|
145
150
|
let pk;
|
|
146
151
|
let m2m;
|
|
147
152
|
let checks;
|
|
153
|
+
let indexes;
|
|
148
154
|
for (const [fieldName, def] of Object.entries(raw)) {
|
|
155
|
+
if (fieldName === 'indexes') {
|
|
156
|
+
if (def !== undefined) {
|
|
157
|
+
if (!Array.isArray(def)) {
|
|
158
|
+
throw new Error(`Table "${accessor}": "indexes" must be an array of index declarations`);
|
|
159
|
+
}
|
|
160
|
+
indexes = def;
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
149
164
|
if (fieldName === 'manyToMany') {
|
|
150
165
|
if (def !== undefined) {
|
|
151
166
|
if (!Array.isArray(def)) {
|
|
@@ -205,6 +220,7 @@ function defineSchema(input, options) {
|
|
|
205
220
|
...(pk && pk.length > 0 ? { primaryKey: pk } : {}),
|
|
206
221
|
...(m2m && m2m.length > 0 ? { manyToMany: m2m } : {}),
|
|
207
222
|
...(checks && checks.length > 0 ? { checks } : {}),
|
|
223
|
+
...(indexes && indexes.length > 0 ? { indexes } : {}),
|
|
208
224
|
};
|
|
209
225
|
}
|
|
210
226
|
}
|
|
@@ -25,10 +25,12 @@
|
|
|
25
25
|
* the same conservative auto-`manyToMany` treatment as introspection.
|
|
26
26
|
* - Explicit `manyToMany` declarations on the SchemaDef are merged via
|
|
27
27
|
* {@link applyManyToManyRelations} (additive, never clobbering).
|
|
28
|
-
* - `indexes`
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* - `indexes` carries any declared `TableDef.indexes` (plain column and/or
|
|
29
|
+
* PowDB doc-field expression indexes); a table with none declared gets
|
|
30
|
+
* `[]`, which keeps `schemaHasIndexInfo()` false so the index advisor and
|
|
31
|
+
* the dev-mode missing-index warning stay silent instead of producing
|
|
32
|
+
* blanket false positives. Doc-field (docPath) indexes are ignored by the
|
|
33
|
+
* advisor, so a doc-only index set never flips `schemaHasIndexInfo()`.
|
|
32
34
|
*
|
|
33
35
|
* @example
|
|
34
36
|
* ```ts
|
|
@@ -45,6 +47,7 @@
|
|
|
45
47
|
*/
|
|
46
48
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
49
|
exports.schemaDefToMetadata = schemaDefToMetadata;
|
|
50
|
+
const errors_js_1 = require("./errors.js");
|
|
48
51
|
const introspect_js_1 = require("./introspect.js");
|
|
49
52
|
const schema_js_1 = require("./schema.js");
|
|
50
53
|
const schema_builder_js_1 = require("./schema-builder.js");
|
|
@@ -97,6 +100,67 @@ function resolveColumnName(raw, target) {
|
|
|
97
100
|
return (0, schema_js_1.camelToSnake)(raw);
|
|
98
101
|
}
|
|
99
102
|
// ---------------------------------------------------------------------------
|
|
103
|
+
// Index declarations → IndexMetadata
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
/**
|
|
106
|
+
* Render a doc-field JSON path into an illustrative PowQL fragment for the
|
|
107
|
+
* `IndexMetadata.definition` field (debuggability only; the authoritative,
|
|
108
|
+
* lexer-exact emission lives in `powqlSchemaDDL`). String segments are shown
|
|
109
|
+
* double-quoted, integer array indexes bare.
|
|
110
|
+
*/
|
|
111
|
+
function docPathFragment(column, path) {
|
|
112
|
+
const segs = path.map((s) => (typeof s === 'number' ? `->${s}` : `->"${s}"`)).join('');
|
|
113
|
+
return `(.${column}${segs})`;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Convert a table's {@link SchemaIndexDef} list into {@link IndexMetadata}.
|
|
117
|
+
* A doc-field index carries `docPath` and `columns: [<json column>]`; a plain
|
|
118
|
+
* column index carries its snake_case column list and no `docPath`. Names are
|
|
119
|
+
* auto-derived (`<table>_<cols>_idx`) when not supplied.
|
|
120
|
+
*/
|
|
121
|
+
function mapIndexes(tableDef, declared) {
|
|
122
|
+
if (!declared || declared.length === 0)
|
|
123
|
+
return [];
|
|
124
|
+
const out = [];
|
|
125
|
+
for (const idx of declared) {
|
|
126
|
+
if ((0, schema_builder_js_1.isDocFieldIndexDef)(idx)) {
|
|
127
|
+
const column = (0, schema_js_1.camelToSnake)(idx.docField);
|
|
128
|
+
const segPart = idx.path.map((s) => (typeof s === 'number' ? String(s) : s)).join('_');
|
|
129
|
+
const name = idx.name ?? `${tableDef.name}_${column}_${segPart}_idx`;
|
|
130
|
+
// Validate numeric (array-index) segments up front: PowDB rejects a
|
|
131
|
+
// negative / fractional / NaN JSON-path index at migration time with an
|
|
132
|
+
// opaque parse error, so fail here with a typed ValidationError naming the
|
|
133
|
+
// index instead of emitting malformed PowQL later.
|
|
134
|
+
for (const seg of idx.path) {
|
|
135
|
+
if (typeof seg === 'number' && (!Number.isInteger(seg) || seg < 0)) {
|
|
136
|
+
throw new errors_js_1.ValidationError(`[turbine] Doc-field index "${name}" on "${tableDef.name}": array-index path segment ${seg} must be a ` +
|
|
137
|
+
'non-negative integer (a JSON array index). Use a string for an object key.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
out.push({
|
|
141
|
+
name,
|
|
142
|
+
columns: [column],
|
|
143
|
+
unique: idx.unique ?? false,
|
|
144
|
+
definition: `${idx.unique ? 'unique ' : 'index '}${docPathFragment(column, idx.path)}`,
|
|
145
|
+
docPath: [...idx.path],
|
|
146
|
+
declared: true,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const columns = idx.columns.map(schema_js_1.camelToSnake);
|
|
151
|
+
const name = idx.name ?? `${tableDef.name}_${columns.join('_')}_idx`;
|
|
152
|
+
out.push({
|
|
153
|
+
name,
|
|
154
|
+
columns,
|
|
155
|
+
unique: idx.unique ?? false,
|
|
156
|
+
definition: `${idx.unique ? 'unique ' : 'index '}(${columns.join(', ')})`,
|
|
157
|
+
declared: true,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
100
164
|
// The converter
|
|
101
165
|
// ---------------------------------------------------------------------------
|
|
102
166
|
/**
|
|
@@ -122,10 +186,14 @@ function resolveColumnName(raw, target) {
|
|
|
122
186
|
* - Explicit `manyToMany` declarations → merged additively.
|
|
123
187
|
* - Schema-level `enums`.
|
|
124
188
|
*
|
|
189
|
+
* What maps (continued):
|
|
190
|
+
* - `indexes` → declared `TableDef.indexes` become `IndexMetadata` (plain
|
|
191
|
+
* column indexes and PowDB doc-field expression indexes, the latter
|
|
192
|
+
* carrying `docPath`). A table with no declared indexes gets `[]`, keeping
|
|
193
|
+
* `schemaHasIndexInfo()` false so index-advisor consumers produce no false
|
|
194
|
+
* positives on index-less code-first metadata.
|
|
195
|
+
*
|
|
125
196
|
* What SchemaDef cannot express (and how it degrades):
|
|
126
|
-
* - Indexes → every table gets `indexes: []`, which keeps
|
|
127
|
-
* `schemaHasIndexInfo()` false so index-advisor consumers produce no
|
|
128
|
-
* false positives on code-first metadata.
|
|
129
197
|
* - Views → never marked (`isView` is introspection-only).
|
|
130
198
|
* - Composite foreign keys → `references:` is single-column by design.
|
|
131
199
|
*/
|
|
@@ -302,9 +370,12 @@ function schemaDefToMetadata(def) {
|
|
|
302
370
|
primaryKey: pk,
|
|
303
371
|
uniqueColumns,
|
|
304
372
|
relations: relationsByTable.get(tableDef.name) ?? {},
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
indexes
|
|
373
|
+
// Declared `indexes` (plain column + doc-field expression) carry through;
|
|
374
|
+
// an undeclared table gets `[]`, which keeps schemaHasIndexInfo() false so
|
|
375
|
+
// the index advisor stays silent. Doc-field (docPath) indexes are ignored
|
|
376
|
+
// by the advisor entirely (see index-advisor.ts), so a doc-only index set
|
|
377
|
+
// never flips schemaHasIndexInfo() and never produces FK false positives.
|
|
378
|
+
indexes: mapIndexes(tableDef, tableDef.indexes),
|
|
308
379
|
};
|
|
309
380
|
}
|
|
310
381
|
const enums = {};
|
package/dist/cjs/sqlite.js
CHANGED
|
@@ -378,6 +378,8 @@ exports.sqliteDialect = {
|
|
|
378
378
|
supportsListenNotify: false,
|
|
379
379
|
supportsRLS: false,
|
|
380
380
|
supportsAdvisoryLock: false,
|
|
381
|
+
// No FROM-clause LATERAL: the opt-in lateral pick plan is Postgres-only.
|
|
382
|
+
supportsLateralJoin: false,
|
|
381
383
|
// json_group_array / json_object have no inline ORDER BY argument, so every
|
|
382
384
|
// ordered to-many relation is forced through the inner-subquery rewrite.
|
|
383
385
|
aggSupportsInlineOrderBy: false,
|
package/dist/dialect.d.ts
CHANGED
|
@@ -295,6 +295,13 @@ export interface Dialect {
|
|
|
295
295
|
readonly supportsRLS: boolean;
|
|
296
296
|
/** Whether this dialect/engine supports advisory-lock-style migration locking. */
|
|
297
297
|
readonly supportsAdvisoryLock: boolean;
|
|
298
|
+
/**
|
|
299
|
+
* Whether this dialect supports `LEFT JOIN LATERAL (...) ON true` in the FROM
|
|
300
|
+
* clause. Gates the opt-in `plan: 'lateral'` pick-row ordering. Optional:
|
|
301
|
+
* absent is treated as `false`, so only dialects that set it true admit the
|
|
302
|
+
* lateral plan (else E017). PostgreSQL only in this release.
|
|
303
|
+
*/
|
|
304
|
+
readonly supportsLateralJoin?: boolean;
|
|
298
305
|
/** Build a dialect-specific RETURNING clause. Return an empty string when unsupported. */
|
|
299
306
|
buildReturningClause(selection?: string): string;
|
|
300
307
|
/** Build a single-row INSERT statement. Inputs are SQL-ready quoted fragments. */
|
package/dist/dialect.js
CHANGED
package/dist/index-advisor.d.ts
CHANGED
|
@@ -58,7 +58,21 @@ export declare function isProbeIndexed(meta: TableMetadata, columns: string[]):
|
|
|
58
58
|
* should gate on {@link schemaHasIndexInfo} to avoid blanket false positives.
|
|
59
59
|
*/
|
|
60
60
|
export declare function findMissingRelationIndexes(schema: SchemaMetadata): MissingRelationIndex[];
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* True when at least one table in the schema carries real, DB-backed index
|
|
63
|
+
* metadata (i.e. from introspection).
|
|
64
|
+
*
|
|
65
|
+
* Excluded (so they never flip the flag, keeping the schema "index-info
|
|
66
|
+
* unknown"):
|
|
67
|
+
* - doc-field expression indexes (`docPath`): they carry no FK-coverage info;
|
|
68
|
+
* - code-first DECLARED indexes (`declared`): the SQL DDL generators do NOT
|
|
69
|
+
* emit `TableDef.indexes` yet, so a declared index does not reflect a real
|
|
70
|
+
* index on the SQL engines. Counting one would arm blanket FK false
|
|
71
|
+
* positives (the FK auto-index the push path DID create is then reported
|
|
72
|
+
* "missing") and, inversely, suppress warnings for indexes never created.
|
|
73
|
+
* A pure code-first schema therefore stays silent exactly as it did before
|
|
74
|
+
* `TableDef.indexes` existed.
|
|
75
|
+
*/
|
|
62
76
|
export declare function schemaHasIndexInfo(schema: SchemaMetadata): boolean;
|
|
63
77
|
/**
|
|
64
78
|
* Single-relation check for the dev-mode runtime warning: the (table, columns)
|
package/dist/index-advisor.js
CHANGED
|
Binary file
|
package/dist/index.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export { type AggregateArgs, type AggregateResult, type ArrayFilter, type Column
|
|
|
47
47
|
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
48
48
|
export type { CheckMetadata, ColumnMetadata, IndexMetadata, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
49
49
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
50
|
-
export { applyManyToManyRelations, type CheckDef, ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnType, type ColumnTypeName, column, type DefineSchemaOptions, defineSchema, type ManyToManyDef, type ReferenceDef, type SchemaDef, type TableDef, table, } from './schema-builder.js';
|
|
50
|
+
export { applyManyToManyRelations, type CheckDef, ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnIndexDef, type ColumnType, type ColumnTypeName, column, type DefineSchemaOptions, type DocFieldIndexDef, defineSchema, isDocFieldIndexDef, type ManyToManyDef, type ReferenceDef, type SchemaDef, type SchemaIndexDef, type TableDef, table, } from './schema-builder.js';
|
|
51
51
|
export { schemaDefToMetadata } from './schema-metadata.js';
|
|
52
52
|
export { type AlterColumnDef, type AlterDef, type DiffResult, type PushResult, type SchemaSqlOptions, schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
|
|
53
53
|
export { type DefinedSeed, defineSeed, type SeedFunction } from './seed.js';
|
package/dist/index.js
CHANGED
|
@@ -53,7 +53,7 @@ export { validateChannel } from './realtime.js';
|
|
|
53
53
|
// Schema utilities
|
|
54
54
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
55
55
|
// Schema builder — define schemas in TypeScript
|
|
56
|
-
export { applyManyToManyRelations, ColumnBuilder, column, defineSchema,
|
|
56
|
+
export { applyManyToManyRelations, ColumnBuilder, column, defineSchema, isDocFieldIndexDef,
|
|
57
57
|
// Legacy compat (deprecated — use object format with defineSchema)
|
|
58
58
|
table, } from './schema-builder.js';
|
|
59
59
|
// Schema metadata bridge — defineSchema() → SchemaMetadata without a live DB
|
package/dist/mssql.js
CHANGED
|
@@ -467,6 +467,9 @@ export const mssqlDialect = {
|
|
|
467
467
|
supportsVector: false,
|
|
468
468
|
supportsListenNotify: false,
|
|
469
469
|
supportsRLS: false,
|
|
470
|
+
// SQL Server has OUTER APPLY, not FROM-clause LATERAL: the lateral pick plan
|
|
471
|
+
// is Postgres-only (out of scope here).
|
|
472
|
+
supportsLateralJoin: false,
|
|
470
473
|
// sp_getapplock / sp_releaseapplock exist (used by a future migrate adapter).
|
|
471
474
|
supportsAdvisoryLock: true,
|
|
472
475
|
// FOR JSON over zero rows is NULL → coalesced in the relation override.
|
package/dist/mysql.js
CHANGED
|
@@ -368,6 +368,9 @@ export const mysqlDialect = {
|
|
|
368
368
|
supportsVector: false,
|
|
369
369
|
supportsListenNotify: false,
|
|
370
370
|
supportsRLS: false,
|
|
371
|
+
// MySQL 8.0.14+ supports LATERAL, but the opt-in lateral pick plan stays
|
|
372
|
+
// Postgres-only in this release (flipping it on is a one-line change + tests).
|
|
373
|
+
supportsLateralJoin: false,
|
|
371
374
|
// GET_LOCK / RELEASE_LOCK exist (used by a future migrate adapter).
|
|
372
375
|
supportsAdvisoryLock: true,
|
|
373
376
|
// JSON_ARRAYAGG has no inline ORDER BY argument → force the inner-subquery
|
|
@@ -86,4 +86,32 @@ async function importOptionalPeer(specifier, allowEsmFallback = true) {
|
|
|
86
86
|
return esmCapableCopy(specifier, false);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Merged namespace so callers can reach {@link peerPackageVersion} off the same
|
|
91
|
+
* default import (`importOptionalPeer.peerPackageVersion(...)`). Lives in this
|
|
92
|
+
* `.cts` file for the same reason the dynamic import does: a `.cts` compiles to
|
|
93
|
+
* CommonJS in BOTH build passes, so `require` / `require.resolve` are natively
|
|
94
|
+
* available and `import.meta` is never emitted (which would break the CJS build
|
|
95
|
+
* and crash CJS consumers, see `resolveEmbeddedVersion` in powdb.ts).
|
|
96
|
+
*/
|
|
97
|
+
(function (importOptionalPeer) {
|
|
98
|
+
/**
|
|
99
|
+
* Resolve an optional peer's declared `package.json` version WITHOUT loading
|
|
100
|
+
* the package itself (so an ESM-only peer never trips `require`). `require` is
|
|
101
|
+
* anchored on THIS module's location (inside the published `dist/`), so bare
|
|
102
|
+
* resolution walks up `node_modules` and finds the peer exactly where
|
|
103
|
+
* `import.meta.url` used to point, but it compiles under `module: CommonJS`
|
|
104
|
+
* too. Returns `null` when the peer / its package.json cannot be resolved.
|
|
105
|
+
*/
|
|
106
|
+
function peerPackageVersion(specifier) {
|
|
107
|
+
try {
|
|
108
|
+
const pkg = require(`${specifier}/package.json`);
|
|
109
|
+
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
importOptionalPeer.peerPackageVersion = peerPackageVersion;
|
|
116
|
+
})(importOptionalPeer || (importOptionalPeer = {}));
|
|
89
117
|
module.exports = importOptionalPeer;
|