@strapi/database 4.5.0-alpha.0 → 4.5.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/lib/entity-manager/entity-repository.js +51 -12
- package/lib/entity-manager/index.js +338 -543
- package/lib/entity-manager/morph-relations.js +6 -2
- package/lib/entity-manager/regular-relations.js +281 -0
- package/lib/metadata/index.js +25 -2
- package/lib/metadata/relations.js +15 -2
- package/lib/query/helpers/populate/apply.js +646 -0
- package/lib/query/helpers/populate/index.js +9 -0
- package/lib/query/helpers/populate/process.js +102 -0
- package/lib/query/query-builder.js +29 -0
- package/lib/tests/{knex-utils.test.e2e.js → knex-utils.test.api.js} +0 -0
- package/package.json +2 -2
- package/lib/query/helpers/populate.js +0 -649
|
@@ -20,7 +20,7 @@ const getMorphToManyRowsLinkedToMorphOne = (rows, { uid, attributeName, typeColu
|
|
|
20
20
|
|
|
21
21
|
const deleteRelatedMorphOneRelationsAfterMorphToManyUpdate = async (
|
|
22
22
|
rows,
|
|
23
|
-
{ uid, attributeName, joinTable, db }
|
|
23
|
+
{ uid, attributeName, joinTable, db, transaction: trx }
|
|
24
24
|
) => {
|
|
25
25
|
const { morphColumn } = joinTable;
|
|
26
26
|
const { idColumn, typeColumn } = morphColumn;
|
|
@@ -50,7 +50,11 @@ const deleteRelatedMorphOneRelationsAfterMorphToManyUpdate = async (
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
if (!isEmpty(orWhere)) {
|
|
53
|
-
await createQueryBuilder(joinTable.name, db)
|
|
53
|
+
await createQueryBuilder(joinTable.name, db)
|
|
54
|
+
.delete()
|
|
55
|
+
.where({ $or: orWhere })
|
|
56
|
+
.transacting(trx)
|
|
57
|
+
.execute();
|
|
54
58
|
}
|
|
55
59
|
};
|
|
56
60
|
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { map, isEmpty } = require('lodash/fp');
|
|
4
|
+
const {
|
|
5
|
+
isBidirectional,
|
|
6
|
+
isOneToAny,
|
|
7
|
+
isManyToAny,
|
|
8
|
+
isAnyToOne,
|
|
9
|
+
hasOrderColumn,
|
|
10
|
+
hasInverseOrderColumn,
|
|
11
|
+
} = require('../metadata/relations');
|
|
12
|
+
const { createQueryBuilder } = require('../query');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* If some relations currently exist for this oneToX relation, on the one side, this function removes them and update the inverse order if needed.
|
|
16
|
+
* @param {Object} params
|
|
17
|
+
* @param {string} params.id - entity id on which the relations for entities relIdsToadd are created
|
|
18
|
+
* @param {string} params.attribute - attribute of the relation
|
|
19
|
+
* @param {string} params.inverseRelIds - entity ids of the inverse side for which the current relations will be deleted
|
|
20
|
+
* @param {string} params.db - database instance
|
|
21
|
+
*/
|
|
22
|
+
const deletePreviousOneToAnyRelations = async ({
|
|
23
|
+
id,
|
|
24
|
+
attribute,
|
|
25
|
+
relIdsToadd,
|
|
26
|
+
db,
|
|
27
|
+
transaction: trx,
|
|
28
|
+
}) => {
|
|
29
|
+
if (!(isBidirectional(attribute) && isOneToAny(attribute))) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'deletePreviousOneToAnyRelations can only be called for bidirectional oneToAny relations'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const { joinTable } = attribute;
|
|
35
|
+
const { joinColumn, inverseJoinColumn } = joinTable;
|
|
36
|
+
|
|
37
|
+
await createQueryBuilder(joinTable.name, db)
|
|
38
|
+
.delete()
|
|
39
|
+
.where({
|
|
40
|
+
[inverseJoinColumn.name]: relIdsToadd,
|
|
41
|
+
[joinColumn.name]: { $ne: id },
|
|
42
|
+
})
|
|
43
|
+
.where(joinTable.on || {})
|
|
44
|
+
.transacting(trx)
|
|
45
|
+
.execute();
|
|
46
|
+
|
|
47
|
+
await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToadd, transaction: trx });
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* If a relation currently exists for this xToOne relations, this function removes it and update the inverse order if needed.
|
|
52
|
+
* @param {Object} params
|
|
53
|
+
* @param {string} params.id - entity id on which the relation for entity relIdToadd is created
|
|
54
|
+
* @param {string} params.attribute - attribute of the relation
|
|
55
|
+
* @param {string} params.relIdToadd - entity id of the new relation
|
|
56
|
+
* @param {string} params.db - database instance
|
|
57
|
+
*/
|
|
58
|
+
const deletePreviousAnyToOneRelations = async ({
|
|
59
|
+
id,
|
|
60
|
+
attribute,
|
|
61
|
+
relIdToadd,
|
|
62
|
+
db,
|
|
63
|
+
transaction: trx,
|
|
64
|
+
}) => {
|
|
65
|
+
const { joinTable } = attribute;
|
|
66
|
+
const { joinColumn, inverseJoinColumn } = joinTable;
|
|
67
|
+
|
|
68
|
+
if (!isAnyToOne(attribute)) {
|
|
69
|
+
throw new Error('deletePreviousAnyToOneRelations can only be called for anyToOne relations');
|
|
70
|
+
}
|
|
71
|
+
// handling manyToOne
|
|
72
|
+
if (isManyToAny(attribute)) {
|
|
73
|
+
// if the database integrity was not broken relsToDelete is supposed to be of length 1
|
|
74
|
+
const relsToDelete = await createQueryBuilder(joinTable.name, db)
|
|
75
|
+
.select(inverseJoinColumn.name)
|
|
76
|
+
.where({
|
|
77
|
+
[joinColumn.name]: id,
|
|
78
|
+
[inverseJoinColumn.name]: { $ne: relIdToadd },
|
|
79
|
+
})
|
|
80
|
+
.where(joinTable.on || {})
|
|
81
|
+
.transacting(trx)
|
|
82
|
+
.execute();
|
|
83
|
+
|
|
84
|
+
const relIdsToDelete = map(inverseJoinColumn.name, relsToDelete);
|
|
85
|
+
|
|
86
|
+
await createQueryBuilder(joinTable.name, db)
|
|
87
|
+
.delete()
|
|
88
|
+
.where({
|
|
89
|
+
[joinColumn.name]: id,
|
|
90
|
+
[inverseJoinColumn.name]: { $in: relIdsToDelete },
|
|
91
|
+
})
|
|
92
|
+
.where(joinTable.on || {})
|
|
93
|
+
.transacting(trx)
|
|
94
|
+
.execute();
|
|
95
|
+
|
|
96
|
+
await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToDelete, transaction: trx });
|
|
97
|
+
|
|
98
|
+
// handling oneToOne
|
|
99
|
+
} else {
|
|
100
|
+
await createQueryBuilder(joinTable.name, db)
|
|
101
|
+
.delete()
|
|
102
|
+
.where({
|
|
103
|
+
[joinColumn.name]: id,
|
|
104
|
+
[inverseJoinColumn.name]: { $ne: relIdToadd },
|
|
105
|
+
})
|
|
106
|
+
.where(joinTable.on || {})
|
|
107
|
+
.transacting(trx)
|
|
108
|
+
.execute();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Delete all or some relations of entity field
|
|
114
|
+
* @param {Object} params
|
|
115
|
+
* @param {string} params.id - entity id for which the relations will be deleted
|
|
116
|
+
* @param {string} params.attribute - attribute of the relation
|
|
117
|
+
* @param {string} params.db - database instance
|
|
118
|
+
* @param {string} params.relIdsToDelete - ids of entities to remove from the relations. Also accepts 'all'
|
|
119
|
+
* @param {string} params.relIdsToNotDelete - ids of entities to not remove from the relation when relIdsToDelete equals 'all'
|
|
120
|
+
*/
|
|
121
|
+
const deleteRelations = async ({
|
|
122
|
+
id,
|
|
123
|
+
attribute,
|
|
124
|
+
db,
|
|
125
|
+
relIdsToNotDelete = [],
|
|
126
|
+
relIdsToDelete = [],
|
|
127
|
+
transaction: trx,
|
|
128
|
+
}) => {
|
|
129
|
+
const { joinTable } = attribute;
|
|
130
|
+
const { joinColumn, inverseJoinColumn } = joinTable;
|
|
131
|
+
const all = relIdsToDelete === 'all';
|
|
132
|
+
|
|
133
|
+
if (hasOrderColumn(attribute) || hasInverseOrderColumn(attribute)) {
|
|
134
|
+
let lastId = 0;
|
|
135
|
+
let done = false;
|
|
136
|
+
const batchSize = 100;
|
|
137
|
+
while (!done) {
|
|
138
|
+
const batchToDelete = await createQueryBuilder(joinTable.name, db)
|
|
139
|
+
.select(inverseJoinColumn.name)
|
|
140
|
+
.where({
|
|
141
|
+
[joinColumn.name]: id,
|
|
142
|
+
id: { $gt: lastId },
|
|
143
|
+
[inverseJoinColumn.name]: { $notIn: relIdsToNotDelete },
|
|
144
|
+
...(all ? {} : { [inverseJoinColumn.name]: { $in: relIdsToDelete } }),
|
|
145
|
+
})
|
|
146
|
+
.where(joinTable.on || {})
|
|
147
|
+
.orderBy('id')
|
|
148
|
+
.limit(batchSize)
|
|
149
|
+
.transacting(trx)
|
|
150
|
+
.execute();
|
|
151
|
+
done = batchToDelete.length < batchSize;
|
|
152
|
+
lastId = batchToDelete[batchToDelete.length - 1]?.id;
|
|
153
|
+
|
|
154
|
+
const batchIds = map(inverseJoinColumn.name, batchToDelete);
|
|
155
|
+
|
|
156
|
+
await createQueryBuilder(joinTable.name, db)
|
|
157
|
+
.delete()
|
|
158
|
+
.where({
|
|
159
|
+
[joinColumn.name]: id,
|
|
160
|
+
[inverseJoinColumn.name]: { $in: batchIds },
|
|
161
|
+
})
|
|
162
|
+
.where(joinTable.on || {})
|
|
163
|
+
.transacting(trx)
|
|
164
|
+
.execute();
|
|
165
|
+
|
|
166
|
+
await cleanOrderColumns({ attribute, db, id, inverseRelIds: batchIds, transaction: trx });
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
await createQueryBuilder(joinTable.name, db)
|
|
170
|
+
.delete()
|
|
171
|
+
.where({
|
|
172
|
+
[joinColumn.name]: id,
|
|
173
|
+
[inverseJoinColumn.name]: { $notIn: relIdsToNotDelete },
|
|
174
|
+
...(all ? {} : { [inverseJoinColumn.name]: { $in: relIdsToDelete } }),
|
|
175
|
+
})
|
|
176
|
+
.where(joinTable.on || {})
|
|
177
|
+
.transacting(trx)
|
|
178
|
+
.execute();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Clean the order columns by ensuring the order value are continuous (ex: 1, 2, 3 and not 1, 5, 10)
|
|
184
|
+
* @param {Object} params
|
|
185
|
+
* @param {string} params.id - entity id for which the clean will be done
|
|
186
|
+
* @param {string} params.attribute - attribute of the relation
|
|
187
|
+
* @param {string} params.db - database instance
|
|
188
|
+
* @param {string} params.inverseRelIds - entity ids of the inverse side for which the clean will be done
|
|
189
|
+
*/
|
|
190
|
+
const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction: trx }) => {
|
|
191
|
+
if (
|
|
192
|
+
!(hasOrderColumn(attribute) && id) &&
|
|
193
|
+
!(hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds))
|
|
194
|
+
) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { joinTable } = attribute;
|
|
199
|
+
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
|
|
200
|
+
const update = [];
|
|
201
|
+
const updateBinding = [];
|
|
202
|
+
const select = ['??'];
|
|
203
|
+
const selectBinding = ['id'];
|
|
204
|
+
const where = [];
|
|
205
|
+
const whereBinding = [];
|
|
206
|
+
|
|
207
|
+
if (hasOrderColumn(attribute) && id) {
|
|
208
|
+
update.push('?? = b.src_order');
|
|
209
|
+
updateBinding.push(orderColumnName);
|
|
210
|
+
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS src_order');
|
|
211
|
+
selectBinding.push(joinColumn.name, orderColumnName);
|
|
212
|
+
where.push('?? = ?');
|
|
213
|
+
whereBinding.push(joinColumn.name, id);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
|
|
217
|
+
update.push('?? = b.inv_order');
|
|
218
|
+
updateBinding.push(inverseOrderColumnName);
|
|
219
|
+
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS inv_order');
|
|
220
|
+
selectBinding.push(inverseJoinColumn.name, inverseOrderColumnName);
|
|
221
|
+
where.push(`?? IN (${inverseRelIds.map(() => '?').join(', ')})`);
|
|
222
|
+
whereBinding.push(inverseJoinColumn.name, ...inverseRelIds);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// raw query as knex doesn't allow updating from a subquery
|
|
226
|
+
// https://github.com/knex/knex/issues/2504
|
|
227
|
+
switch (strapi.db.dialect.client) {
|
|
228
|
+
case 'mysql':
|
|
229
|
+
await db
|
|
230
|
+
.getConnection()
|
|
231
|
+
.raw(
|
|
232
|
+
`UPDATE
|
|
233
|
+
?? as a,
|
|
234
|
+
(
|
|
235
|
+
SELECT ${select.join(', ')}
|
|
236
|
+
FROM ??
|
|
237
|
+
WHERE ${where.join(' OR ')}
|
|
238
|
+
) AS b
|
|
239
|
+
SET ${update.join(', ')}
|
|
240
|
+
WHERE b.id = a.id`,
|
|
241
|
+
[joinTable.name, ...selectBinding, joinTable.name, ...whereBinding, ...updateBinding]
|
|
242
|
+
)
|
|
243
|
+
.transacting(trx);
|
|
244
|
+
break;
|
|
245
|
+
default:
|
|
246
|
+
await db
|
|
247
|
+
.getConnection()
|
|
248
|
+
.raw(
|
|
249
|
+
`UPDATE ?? as a
|
|
250
|
+
SET ${update.join(', ')}
|
|
251
|
+
FROM (
|
|
252
|
+
SELECT ${select.join(', ')}
|
|
253
|
+
FROM ??
|
|
254
|
+
WHERE ${where.join(' OR ')}
|
|
255
|
+
) AS b
|
|
256
|
+
WHERE b.id = a.id`,
|
|
257
|
+
[joinTable.name, ...updateBinding, ...selectBinding, joinTable.name, ...whereBinding]
|
|
258
|
+
)
|
|
259
|
+
.transacting(trx);
|
|
260
|
+
/*
|
|
261
|
+
`UPDATE :joinTable: as a
|
|
262
|
+
SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order
|
|
263
|
+
FROM (
|
|
264
|
+
SELECT
|
|
265
|
+
id,
|
|
266
|
+
ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
|
|
267
|
+
ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
|
|
268
|
+
FROM :joinTable:
|
|
269
|
+
WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds)
|
|
270
|
+
) AS b
|
|
271
|
+
WHERE b.id = a.id`,
|
|
272
|
+
*/
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
module.exports = {
|
|
277
|
+
deletePreviousOneToAnyRelations,
|
|
278
|
+
deletePreviousAnyToOneRelations,
|
|
279
|
+
deleteRelations,
|
|
280
|
+
cleanOrderColumns,
|
|
281
|
+
};
|
package/lib/metadata/index.js
CHANGED
|
@@ -9,6 +9,21 @@ class Metadata extends Map {
|
|
|
9
9
|
add(meta) {
|
|
10
10
|
return this.set(meta.uid, meta);
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validate the DB metadata, throwing an error if a duplicate DB table name is detected
|
|
15
|
+
*/
|
|
16
|
+
validate() {
|
|
17
|
+
const seenTables = new Map();
|
|
18
|
+
for (const meta of this.values()) {
|
|
19
|
+
if (seenTables.get(meta.tableName)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`DB table "${meta.tableName}" already exists. Change the collectionName of the related content type.`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
seenTables.set(meta.tableName, true);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
12
27
|
}
|
|
13
28
|
|
|
14
29
|
// TODO: check if there isn't an attribute with an id already
|
|
@@ -81,6 +96,7 @@ const createMetadata = (models = []) => {
|
|
|
81
96
|
meta.columnToAttribute = columnToAttribute;
|
|
82
97
|
}
|
|
83
98
|
|
|
99
|
+
metadata.validate();
|
|
84
100
|
return metadata;
|
|
85
101
|
};
|
|
86
102
|
|
|
@@ -123,7 +139,7 @@ const createCompoLinkModelMeta = (baseModelMeta) => {
|
|
|
123
139
|
type: 'integer',
|
|
124
140
|
column: {
|
|
125
141
|
unsigned: true,
|
|
126
|
-
defaultTo:
|
|
142
|
+
defaultTo: null,
|
|
127
143
|
},
|
|
128
144
|
},
|
|
129
145
|
},
|
|
@@ -142,6 +158,11 @@ const createCompoLinkModelMeta = (baseModelMeta) => {
|
|
|
142
158
|
name: `${baseModelMeta.tableName}_entity_fk`,
|
|
143
159
|
columns: ['entity_id'],
|
|
144
160
|
},
|
|
161
|
+
{
|
|
162
|
+
name: `${baseModelMeta.tableName}_unique`,
|
|
163
|
+
columns: ['entity_id', 'component_id', 'field', 'component_type'],
|
|
164
|
+
type: 'unique',
|
|
165
|
+
},
|
|
145
166
|
],
|
|
146
167
|
foreignKeys: [
|
|
147
168
|
{
|
|
@@ -183,6 +204,7 @@ const createDynamicZone = (attributeName, attribute, meta) => {
|
|
|
183
204
|
orderBy: {
|
|
184
205
|
order: 'asc',
|
|
185
206
|
},
|
|
207
|
+
pivotColumns: ['entity_id', 'component_id', 'field', 'component_type'],
|
|
186
208
|
},
|
|
187
209
|
});
|
|
188
210
|
};
|
|
@@ -205,10 +227,11 @@ const createComponent = (attributeName, attribute, meta) => {
|
|
|
205
227
|
on: {
|
|
206
228
|
field: attributeName,
|
|
207
229
|
},
|
|
230
|
+
orderColumnName: 'order',
|
|
208
231
|
orderBy: {
|
|
209
232
|
order: 'asc',
|
|
210
233
|
},
|
|
211
|
-
|
|
234
|
+
pivotColumns: ['entity_id', 'component_id', 'field', 'component_type'],
|
|
212
235
|
},
|
|
213
236
|
});
|
|
214
237
|
};
|
|
@@ -272,6 +272,7 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => {
|
|
|
272
272
|
orderBy: {
|
|
273
273
|
order: 'asc',
|
|
274
274
|
},
|
|
275
|
+
pivotColumns: [joinColumnName, typeColumnName, idColumnName],
|
|
275
276
|
};
|
|
276
277
|
|
|
277
278
|
attribute.joinTable = joinTable;
|
|
@@ -406,8 +407,8 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
|
|
|
406
407
|
inverseJoinColumnName = `inv_${inverseJoinColumnName}`;
|
|
407
408
|
}
|
|
408
409
|
|
|
409
|
-
const orderColumnName = _.snakeCase(`${
|
|
410
|
-
let inverseOrderColumnName = _.snakeCase(`${
|
|
410
|
+
const orderColumnName = _.snakeCase(`${targetMeta.singularName}_order`);
|
|
411
|
+
let inverseOrderColumnName = _.snakeCase(`${meta.singularName}_order`);
|
|
411
412
|
|
|
412
413
|
// if relation is self referencing
|
|
413
414
|
if (attribute.relation === 'manyToMany' && joinColumnName === inverseJoinColumnName) {
|
|
@@ -444,6 +445,11 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
|
|
|
444
445
|
name: `${joinTableName}_inv_fk`,
|
|
445
446
|
columns: [inverseJoinColumnName],
|
|
446
447
|
},
|
|
448
|
+
{
|
|
449
|
+
name: `${joinTableName}_unique`,
|
|
450
|
+
columns: [joinColumnName, inverseJoinColumnName],
|
|
451
|
+
type: 'unique',
|
|
452
|
+
},
|
|
447
453
|
],
|
|
448
454
|
foreignKeys: [
|
|
449
455
|
{
|
|
@@ -473,6 +479,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
|
|
|
473
479
|
name: inverseJoinColumnName,
|
|
474
480
|
referencedColumn: 'id',
|
|
475
481
|
},
|
|
482
|
+
pivotColumns: [joinColumnName, inverseJoinColumnName],
|
|
476
483
|
};
|
|
477
484
|
|
|
478
485
|
// order
|
|
@@ -527,6 +534,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
|
|
|
527
534
|
name: joinTableName,
|
|
528
535
|
joinColumn: joinTable.inverseJoinColumn,
|
|
529
536
|
inverseJoinColumn: joinTable.joinColumn,
|
|
537
|
+
pivotColumns: joinTable.pivotColumns,
|
|
530
538
|
};
|
|
531
539
|
|
|
532
540
|
if (isManyToAny(attribute)) {
|
|
@@ -539,6 +547,9 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
|
|
|
539
547
|
}
|
|
540
548
|
};
|
|
541
549
|
|
|
550
|
+
const hasOrderColumn = (attribute) => isAnyToMany(attribute);
|
|
551
|
+
const hasInverseOrderColumn = (attribute) => isBidirectional(attribute) && isManyToAny(attribute);
|
|
552
|
+
|
|
542
553
|
module.exports = {
|
|
543
554
|
createRelation,
|
|
544
555
|
|
|
@@ -547,4 +558,6 @@ module.exports = {
|
|
|
547
558
|
isManyToAny,
|
|
548
559
|
isAnyToOne,
|
|
549
560
|
isAnyToMany,
|
|
561
|
+
hasOrderColumn,
|
|
562
|
+
hasInverseOrderColumn,
|
|
550
563
|
};
|