@farming-labs/orm-sql 0.0.11 → 0.0.12
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/index.cjs +266 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +266 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -29,6 +29,7 @@ __export(index_exports, {
|
|
|
29
29
|
module.exports = __toCommonJS(index_exports);
|
|
30
30
|
var import_node_crypto = require("crypto");
|
|
31
31
|
var import_orm = require("@farming-labs/orm");
|
|
32
|
+
var nativeNodeIdentity = /* @__PURE__ */ Symbol("nativeNodeIdentity");
|
|
32
33
|
var manifestCache = /* @__PURE__ */ new WeakMap();
|
|
33
34
|
function getManifest(schema) {
|
|
34
35
|
const cached = manifestCache.get(schema);
|
|
@@ -168,12 +169,12 @@ function mergeWhere(...clauses) {
|
|
|
168
169
|
AND: defined
|
|
169
170
|
};
|
|
170
171
|
}
|
|
171
|
-
function compileFieldFilter(model, fieldName, filter, dialect, state) {
|
|
172
|
+
function compileFieldFilter(model, fieldName, filter, dialect, state, tableAlias = model.table) {
|
|
172
173
|
const field = model.fields[fieldName];
|
|
173
174
|
if (!field) {
|
|
174
175
|
throw new Error(`Unknown field "${fieldName}" on model "${model.name}".`);
|
|
175
176
|
}
|
|
176
|
-
const column = `${quoteIdentifier(
|
|
177
|
+
const column = `${quoteIdentifier(tableAlias, dialect)}.${quoteIdentifier(field.column, dialect)}`;
|
|
177
178
|
const createValueExpression = (value) => {
|
|
178
179
|
const placeholder = createPlaceholder(dialect, state, encodeValue(field, dialect, value));
|
|
179
180
|
if (field.kind === "json" && dialect === "mysql") {
|
|
@@ -235,39 +236,39 @@ function compileFieldFilter(model, fieldName, filter, dialect, state) {
|
|
|
235
236
|
if (clauses.length === 1) return clauses[0];
|
|
236
237
|
return `(${clauses.join(" and ")})`;
|
|
237
238
|
}
|
|
238
|
-
function compileWhere(model, where, dialect, state) {
|
|
239
|
+
function compileWhere(model, where, dialect, state, tableAlias = model.table) {
|
|
239
240
|
if (!where) return void 0;
|
|
240
241
|
const clauses = [];
|
|
241
242
|
for (const [key, value] of Object.entries(where)) {
|
|
242
243
|
if (key === "AND") {
|
|
243
244
|
const items = Array.isArray(value) ? value : [];
|
|
244
245
|
if (!items.length) continue;
|
|
245
|
-
const nested = items.map((item) => compileWhere(model, item, dialect, state)).filter(Boolean).map((item) => `(${item})`);
|
|
246
|
+
const nested = items.map((item) => compileWhere(model, item, dialect, state, tableAlias)).filter(Boolean).map((item) => `(${item})`);
|
|
246
247
|
if (nested.length) clauses.push(nested.join(" and "));
|
|
247
248
|
continue;
|
|
248
249
|
}
|
|
249
250
|
if (key === "OR") {
|
|
250
251
|
const items = Array.isArray(value) ? value : [];
|
|
251
252
|
if (!items.length) continue;
|
|
252
|
-
const nested = items.map((item) => compileWhere(model, item, dialect, state)).filter(Boolean).map((item) => `(${item})`);
|
|
253
|
+
const nested = items.map((item) => compileWhere(model, item, dialect, state, tableAlias)).filter(Boolean).map((item) => `(${item})`);
|
|
253
254
|
if (nested.length) clauses.push(`(${nested.join(" or ")})`);
|
|
254
255
|
continue;
|
|
255
256
|
}
|
|
256
257
|
if (key === "NOT") {
|
|
257
|
-
const nested = compileWhere(model, value, dialect, state);
|
|
258
|
+
const nested = compileWhere(model, value, dialect, state, tableAlias);
|
|
258
259
|
if (nested) clauses.push(`not (${nested})`);
|
|
259
260
|
continue;
|
|
260
261
|
}
|
|
261
|
-
clauses.push(compileFieldFilter(model, key, value, dialect, state));
|
|
262
|
+
clauses.push(compileFieldFilter(model, key, value, dialect, state, tableAlias));
|
|
262
263
|
}
|
|
263
264
|
if (!clauses.length) return void 0;
|
|
264
265
|
return clauses.join(" and ");
|
|
265
266
|
}
|
|
266
|
-
function compileOrderBy(model, orderBy, dialect) {
|
|
267
|
+
function compileOrderBy(model, orderBy, dialect, tableAlias = model.table) {
|
|
267
268
|
if (!orderBy) return "";
|
|
268
269
|
const parts = Object.entries(orderBy).filter(([fieldName]) => fieldName in model.fields).map(([fieldName, direction]) => {
|
|
269
270
|
const field = model.fields[fieldName];
|
|
270
|
-
return `${quoteIdentifier(
|
|
271
|
+
return `${quoteIdentifier(tableAlias, dialect)}.${quoteIdentifier(field.column, dialect)} ${direction === "desc" ? "desc" : "asc"}`;
|
|
271
272
|
});
|
|
272
273
|
if (!parts.length) return "";
|
|
273
274
|
return ` order by ${parts.join(", ")}`;
|
|
@@ -290,13 +291,14 @@ function compilePagination(dialect, take, skip) {
|
|
|
290
291
|
}
|
|
291
292
|
function buildSelectStatement(model, dialect, args) {
|
|
292
293
|
const state = { params: [] };
|
|
294
|
+
const tableAlias = args.tableAlias ?? model.table;
|
|
293
295
|
const selectList = Object.values(model.fields).map(
|
|
294
|
-
(field) => `${quoteIdentifier(
|
|
296
|
+
(field) => `${quoteIdentifier(tableAlias, dialect)}.${quoteIdentifier(field.column, dialect)} as ${quoteIdentifier(field.name, dialect)}`
|
|
295
297
|
);
|
|
296
|
-
let sql = `select ${selectList.join(", ")} from ${quoteIdentifier(model.table, dialect)}`;
|
|
297
|
-
const where = compileWhere(model, args.where, dialect, state);
|
|
298
|
+
let sql = `select ${selectList.join(", ")} from ${quoteIdentifier(model.table, dialect)} as ${quoteIdentifier(tableAlias, dialect)}`;
|
|
299
|
+
const where = compileWhere(model, args.where, dialect, state, tableAlias);
|
|
298
300
|
if (where) sql += ` where ${where}`;
|
|
299
|
-
sql += compileOrderBy(model, args.orderBy, dialect);
|
|
301
|
+
sql += compileOrderBy(model, args.orderBy, dialect, tableAlias);
|
|
300
302
|
sql += compilePagination(dialect, args.take, args.skip);
|
|
301
303
|
return { sql, params: state.params };
|
|
302
304
|
}
|
|
@@ -598,6 +600,10 @@ function createMysqlPoolAdapter(pool) {
|
|
|
598
600
|
}
|
|
599
601
|
function createSqlDriver(adapter) {
|
|
600
602
|
async function loadRows(schema, modelName, args) {
|
|
603
|
+
const nativeRows = await loadRowsWithNativeJoins(schema, modelName, args);
|
|
604
|
+
if (nativeRows) {
|
|
605
|
+
return nativeRows;
|
|
606
|
+
}
|
|
601
607
|
const manifest = getManifest(schema);
|
|
602
608
|
const model = manifest.models[modelName];
|
|
603
609
|
const statement = buildSelectStatement(model, adapter.dialect, args);
|
|
@@ -623,6 +629,253 @@ function createSqlDriver(adapter) {
|
|
|
623
629
|
const row = result.rows[0];
|
|
624
630
|
return row ? decodeRow(model, adapter.dialect, row) : null;
|
|
625
631
|
}
|
|
632
|
+
function createNativePresenceAlias(model, alias, includeAllScalars, selectedScalarKeys) {
|
|
633
|
+
const occupiedAliases = new Set(
|
|
634
|
+
(includeAllScalars ? Object.keys(model.fields) : selectedScalarKeys).map(
|
|
635
|
+
(fieldName) => `${alias}__${fieldName}`
|
|
636
|
+
)
|
|
637
|
+
);
|
|
638
|
+
let candidate = `${alias}__orm_presence`;
|
|
639
|
+
let suffix = 0;
|
|
640
|
+
while (occupiedAliases.has(candidate)) {
|
|
641
|
+
suffix += 1;
|
|
642
|
+
candidate = `${alias}__orm_presence_${suffix}`;
|
|
643
|
+
}
|
|
644
|
+
return candidate;
|
|
645
|
+
}
|
|
646
|
+
function buildNativeJoinPlan(schema, modelName, select, aliasState) {
|
|
647
|
+
const manifest = getManifest(schema);
|
|
648
|
+
const model = manifest.models[modelName];
|
|
649
|
+
const alias = `t${aliasState.next++}`;
|
|
650
|
+
const entries = select ? Object.entries(select) : [];
|
|
651
|
+
const selectedScalarKeys = select ? entries.filter(([key, value]) => key in model.fields && value === true).map(([key]) => key) : Object.keys(model.fields);
|
|
652
|
+
const node = {
|
|
653
|
+
modelName,
|
|
654
|
+
model,
|
|
655
|
+
alias,
|
|
656
|
+
presenceAlias: createNativePresenceAlias(model, alias, !select, selectedScalarKeys),
|
|
657
|
+
includeAllScalars: !select,
|
|
658
|
+
selectedScalarKeys,
|
|
659
|
+
children: []
|
|
660
|
+
};
|
|
661
|
+
for (const [key, value] of entries) {
|
|
662
|
+
if (value === void 0 || !(key in schema.models[modelName].relations)) continue;
|
|
663
|
+
const relation = schema.models[modelName].relations[key];
|
|
664
|
+
const relationArgs = value === true ? {} : value;
|
|
665
|
+
if (relationArgs.where !== void 0 || relationArgs.orderBy !== void 0 || relationArgs.take !== void 0 || relationArgs.skip !== void 0) {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
const child = buildNativeJoinPlan(
|
|
669
|
+
schema,
|
|
670
|
+
relation.target,
|
|
671
|
+
relationArgs.select,
|
|
672
|
+
aliasState
|
|
673
|
+
);
|
|
674
|
+
if (!child) return null;
|
|
675
|
+
child.relationName = key;
|
|
676
|
+
child.relationKind = relation.kind;
|
|
677
|
+
if (relation.kind === "belongsTo") {
|
|
678
|
+
const sourceField = model.fields[relation.foreignKey];
|
|
679
|
+
if (!sourceField) return null;
|
|
680
|
+
const targetReference = parseReference(sourceField.references);
|
|
681
|
+
const targetFieldName = targetReference?.field ?? identityField(manifest.models[relation.target]).name;
|
|
682
|
+
const targetField = child.model.fields[targetFieldName];
|
|
683
|
+
if (!targetField) return null;
|
|
684
|
+
child.sourceField = sourceField;
|
|
685
|
+
child.targetField = targetField;
|
|
686
|
+
} else if (relation.kind === "hasOne" || relation.kind === "hasMany") {
|
|
687
|
+
const targetForeignField = child.model.fields[relation.foreignKey];
|
|
688
|
+
if (!targetForeignField) return null;
|
|
689
|
+
const sourceReference = parseReference(targetForeignField.references);
|
|
690
|
+
const sourceFieldName = sourceReference?.field ?? identityField(manifest.models[modelName]).name;
|
|
691
|
+
const sourceField = model.fields[sourceFieldName];
|
|
692
|
+
if (!sourceField) return null;
|
|
693
|
+
child.sourceField = sourceField;
|
|
694
|
+
child.targetField = targetForeignField;
|
|
695
|
+
} else {
|
|
696
|
+
const throughModel = manifest.models[relation.through];
|
|
697
|
+
const throughFromField = throughModel.fields[relation.from];
|
|
698
|
+
const throughToField = throughModel.fields[relation.to];
|
|
699
|
+
if (!throughFromField || !throughToField) return null;
|
|
700
|
+
const throughFromReference = parseReference(throughFromField.references);
|
|
701
|
+
const throughToReference = parseReference(throughToField.references);
|
|
702
|
+
const sourceFieldName = throughFromReference?.field ?? identityField(manifest.models[modelName]).name;
|
|
703
|
+
const targetFieldName = throughToReference?.field ?? identityField(child.model).name;
|
|
704
|
+
const sourceField = model.fields[sourceFieldName];
|
|
705
|
+
const targetField = child.model.fields[targetFieldName];
|
|
706
|
+
if (!sourceField || !targetField) return null;
|
|
707
|
+
child.sourceField = sourceField;
|
|
708
|
+
child.targetField = targetField;
|
|
709
|
+
child.throughModel = throughModel;
|
|
710
|
+
child.throughAlias = `t${aliasState.next++}`;
|
|
711
|
+
child.throughFromField = throughFromField;
|
|
712
|
+
child.throughToField = throughToField;
|
|
713
|
+
}
|
|
714
|
+
node.children.push(child);
|
|
715
|
+
}
|
|
716
|
+
return node;
|
|
717
|
+
}
|
|
718
|
+
function hasNativeJoinableRelations(schema, modelName, select) {
|
|
719
|
+
if (!select) return false;
|
|
720
|
+
const plan = buildNativeJoinPlan(schema, modelName, select, { next: 0 });
|
|
721
|
+
return !!plan && plan.children.length > 0;
|
|
722
|
+
}
|
|
723
|
+
function collectNativeJoinSelects(node, selectList) {
|
|
724
|
+
const scalarKeys = node.includeAllScalars ? Object.keys(node.model.fields) : node.selectedScalarKeys;
|
|
725
|
+
for (const fieldName of scalarKeys) {
|
|
726
|
+
const field = node.model.fields[fieldName];
|
|
727
|
+
if (!field) continue;
|
|
728
|
+
selectList.push(
|
|
729
|
+
`${quoteIdentifier(node.alias, adapter.dialect)}.${quoteIdentifier(field.column, adapter.dialect)} as ${quoteIdentifier(`${node.alias}__${field.name}`, adapter.dialect)}`
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
const identity = identityField(node.model);
|
|
733
|
+
selectList.push(
|
|
734
|
+
`${quoteIdentifier(node.alias, adapter.dialect)}.${quoteIdentifier(identity.column, adapter.dialect)} as ${quoteIdentifier(node.presenceAlias, adapter.dialect)}`
|
|
735
|
+
);
|
|
736
|
+
for (const child of node.children) {
|
|
737
|
+
collectNativeJoinSelects(child, selectList);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function collectNativeJoinClauses(node, joins) {
|
|
741
|
+
for (const child of node.children) {
|
|
742
|
+
if (child.relationKind === "manyToMany") {
|
|
743
|
+
joins.push(
|
|
744
|
+
` left join ${quoteIdentifier(child.throughModel.table, adapter.dialect)} as ${quoteIdentifier(child.throughAlias, adapter.dialect)} on ${quoteIdentifier(node.alias, adapter.dialect)}.${quoteIdentifier(child.sourceField.column, adapter.dialect)} = ${quoteIdentifier(child.throughAlias, adapter.dialect)}.${quoteIdentifier(child.throughFromField.column, adapter.dialect)}`
|
|
745
|
+
);
|
|
746
|
+
joins.push(
|
|
747
|
+
` left join ${quoteIdentifier(child.model.table, adapter.dialect)} as ${quoteIdentifier(child.alias, adapter.dialect)} on ${quoteIdentifier(child.throughAlias, adapter.dialect)}.${quoteIdentifier(child.throughToField.column, adapter.dialect)} = ${quoteIdentifier(child.alias, adapter.dialect)}.${quoteIdentifier(child.targetField.column, adapter.dialect)}`
|
|
748
|
+
);
|
|
749
|
+
} else {
|
|
750
|
+
const leftColumn = child.relationKind === "belongsTo" ? `${quoteIdentifier(node.alias, adapter.dialect)}.${quoteIdentifier(child.sourceField.column, adapter.dialect)}` : `${quoteIdentifier(child.alias, adapter.dialect)}.${quoteIdentifier(child.targetField.column, adapter.dialect)}`;
|
|
751
|
+
const rightColumn = child.relationKind === "belongsTo" ? `${quoteIdentifier(child.alias, adapter.dialect)}.${quoteIdentifier(child.targetField.column, adapter.dialect)}` : `${quoteIdentifier(node.alias, adapter.dialect)}.${quoteIdentifier(child.sourceField.column, adapter.dialect)}`;
|
|
752
|
+
joins.push(
|
|
753
|
+
` left join ${quoteIdentifier(child.model.table, adapter.dialect)} as ${quoteIdentifier(child.alias, adapter.dialect)} on ${leftColumn} = ${rightColumn}`
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
collectNativeJoinClauses(child, joins);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function buildNativeJoinRootSource(root, args) {
|
|
760
|
+
const state = { params: [] };
|
|
761
|
+
const sourceAlias = `${root.alias}__src`;
|
|
762
|
+
const selectList = Object.values(root.model.fields).map(
|
|
763
|
+
(field) => `${quoteIdentifier(sourceAlias, adapter.dialect)}.${quoteIdentifier(field.column, adapter.dialect)} as ${quoteIdentifier(field.column, adapter.dialect)}`
|
|
764
|
+
);
|
|
765
|
+
let sql = `select ${selectList.join(", ")} from ${quoteIdentifier(root.model.table, adapter.dialect)} as ${quoteIdentifier(sourceAlias, adapter.dialect)}`;
|
|
766
|
+
const where = compileWhere(root.model, args.where, adapter.dialect, state, sourceAlias);
|
|
767
|
+
if (where) sql += ` where ${where}`;
|
|
768
|
+
sql += compileOrderBy(root.model, args.orderBy, adapter.dialect, sourceAlias);
|
|
769
|
+
sql += compilePagination(adapter.dialect, args.take, args.skip);
|
|
770
|
+
return {
|
|
771
|
+
sql: `(${sql})`,
|
|
772
|
+
params: state.params
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function buildNativeJoinStatement(root, args) {
|
|
776
|
+
const state = { params: [] };
|
|
777
|
+
const selectList = [];
|
|
778
|
+
const joins = [];
|
|
779
|
+
collectNativeJoinSelects(root, selectList);
|
|
780
|
+
collectNativeJoinClauses(root, joins);
|
|
781
|
+
const rootSource = buildNativeJoinRootSource(root, args);
|
|
782
|
+
state.params.push(...rootSource.params);
|
|
783
|
+
let sql = `select ${selectList.join(", ")} from ${rootSource.sql} as ${quoteIdentifier(root.alias, adapter.dialect)}`;
|
|
784
|
+
if (joins.length) sql += joins.join("");
|
|
785
|
+
sql += compileOrderBy(root.model, args.orderBy, adapter.dialect, root.alias);
|
|
786
|
+
return { sql, params: state.params };
|
|
787
|
+
}
|
|
788
|
+
function nodePresenceValue(node, rawRow) {
|
|
789
|
+
return rawRow[node.presenceAlias];
|
|
790
|
+
}
|
|
791
|
+
function projectNativeJoinNode(node, rawRow) {
|
|
792
|
+
if (nodePresenceValue(node, rawRow) == null) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
const output = {};
|
|
796
|
+
Object.defineProperty(output, nativeNodeIdentity, {
|
|
797
|
+
value: rawRow[node.presenceAlias],
|
|
798
|
+
enumerable: false,
|
|
799
|
+
configurable: true
|
|
800
|
+
});
|
|
801
|
+
const scalarKeys = node.includeAllScalars ? Object.keys(node.model.fields) : node.selectedScalarKeys;
|
|
802
|
+
for (const fieldName of scalarKeys) {
|
|
803
|
+
const field = node.model.fields[fieldName];
|
|
804
|
+
if (!field) continue;
|
|
805
|
+
output[field.name] = decodeValue(
|
|
806
|
+
field,
|
|
807
|
+
adapter.dialect,
|
|
808
|
+
rawRow[`${node.alias}__${field.name}`]
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
for (const child of node.children) {
|
|
812
|
+
const childValue = projectNativeJoinNode(child, rawRow);
|
|
813
|
+
output[child.relationName] = child.relationKind === "hasMany" || child.relationKind === "manyToMany" ? childValue ? [childValue] : [] : childValue;
|
|
814
|
+
}
|
|
815
|
+
return output;
|
|
816
|
+
}
|
|
817
|
+
function mergeNativeJoinNode(node, target, next) {
|
|
818
|
+
for (const child of node.children) {
|
|
819
|
+
const relationName = child.relationName;
|
|
820
|
+
if (child.relationKind === "hasMany" || child.relationKind === "manyToMany") {
|
|
821
|
+
const targetRows = Array.isArray(target[relationName]) ? target[relationName] : [];
|
|
822
|
+
const nextRows = Array.isArray(next[relationName]) ? next[relationName] : [];
|
|
823
|
+
if (!Array.isArray(target[relationName])) {
|
|
824
|
+
target[relationName] = targetRows;
|
|
825
|
+
}
|
|
826
|
+
for (const nextRow of nextRows) {
|
|
827
|
+
const identity = nextRow[nativeNodeIdentity];
|
|
828
|
+
const existing2 = targetRows.find((entry) => entry[nativeNodeIdentity] === identity);
|
|
829
|
+
if (existing2) {
|
|
830
|
+
mergeNativeJoinNode(child, existing2, nextRow);
|
|
831
|
+
} else {
|
|
832
|
+
targetRows.push(nextRow);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
const nextValue = next[relationName];
|
|
838
|
+
if (nextValue === void 0) continue;
|
|
839
|
+
if (nextValue === null) {
|
|
840
|
+
if (!(relationName in target)) {
|
|
841
|
+
target[relationName] = null;
|
|
842
|
+
}
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
const existing = target[relationName];
|
|
846
|
+
if (!existing || typeof existing !== "object") {
|
|
847
|
+
target[relationName] = nextValue;
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
mergeNativeJoinNode(child, existing, nextValue);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async function loadRowsWithNativeJoins(schema, modelName, args) {
|
|
854
|
+
if (!hasNativeJoinableRelations(schema, modelName, args.select)) {
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
const plan = buildNativeJoinPlan(schema, modelName, args.select, { next: 0 });
|
|
858
|
+
if (!plan || !plan.children.length) {
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
const statement = buildNativeJoinStatement(plan, args);
|
|
862
|
+
const result = await adapter.query(statement.sql, statement.params);
|
|
863
|
+
const groupedRows = [];
|
|
864
|
+
const groupedByIdentity = /* @__PURE__ */ new Map();
|
|
865
|
+
for (const row of result.rows) {
|
|
866
|
+
const projected = projectNativeJoinNode(plan, row);
|
|
867
|
+
if (!projected) continue;
|
|
868
|
+
const identity = projected[nativeNodeIdentity];
|
|
869
|
+
const existing = groupedByIdentity.get(identity);
|
|
870
|
+
if (existing) {
|
|
871
|
+
mergeNativeJoinNode(plan, existing, projected);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
groupedByIdentity.set(identity, projected);
|
|
875
|
+
groupedRows.push(projected);
|
|
876
|
+
}
|
|
877
|
+
return groupedRows;
|
|
878
|
+
}
|
|
626
879
|
async function projectRow(schema, modelName, row, select) {
|
|
627
880
|
const manifest = getManifest(schema);
|
|
628
881
|
const model = manifest.models[modelName];
|