@strapi/database 4.0.0-beta.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/LICENSE +22 -0
- package/examples/connections.js +36 -0
- package/examples/data.sqlite +0 -0
- package/examples/docker-compose.yml +29 -0
- package/examples/index.js +73 -0
- package/examples/models.js +341 -0
- package/examples/typings.ts +17 -0
- package/lib/dialects/dialect.js +45 -0
- package/lib/dialects/index.js +28 -0
- package/lib/dialects/mysql/index.js +51 -0
- package/lib/dialects/mysql/schema-inspector.js +203 -0
- package/lib/dialects/postgresql/index.js +49 -0
- package/lib/dialects/postgresql/schema-inspector.js +229 -0
- package/lib/dialects/sqlite/index.js +74 -0
- package/lib/dialects/sqlite/schema-inspector.js +151 -0
- package/lib/entity-manager.js +886 -0
- package/lib/entity-repository.js +110 -0
- package/lib/errors.js +14 -0
- package/lib/fields.d.ts +9 -0
- package/lib/fields.js +232 -0
- package/lib/index.d.ts +146 -0
- package/lib/index.js +60 -0
- package/lib/lifecycles/index.d.ts +50 -0
- package/lib/lifecycles/index.js +66 -0
- package/lib/lifecycles/subscribers/index.d.ts +9 -0
- package/lib/lifecycles/subscribers/models-lifecycles.js +19 -0
- package/lib/lifecycles/subscribers/timestamps.js +65 -0
- package/lib/metadata/index.js +219 -0
- package/lib/metadata/relations.js +488 -0
- package/lib/migrations/index.d.ts +9 -0
- package/lib/migrations/index.js +69 -0
- package/lib/migrations/storage.js +49 -0
- package/lib/query/helpers/index.js +10 -0
- package/lib/query/helpers/join.js +95 -0
- package/lib/query/helpers/order-by.js +70 -0
- package/lib/query/helpers/populate.js +652 -0
- package/lib/query/helpers/search.js +84 -0
- package/lib/query/helpers/transform.js +84 -0
- package/lib/query/helpers/where.js +322 -0
- package/lib/query/index.js +7 -0
- package/lib/query/query-builder.js +348 -0
- package/lib/schema/__tests__/schema-diff.test.js +181 -0
- package/lib/schema/builder.js +352 -0
- package/lib/schema/diff.js +376 -0
- package/lib/schema/index.d.ts +49 -0
- package/lib/schema/index.js +95 -0
- package/lib/schema/schema.js +209 -0
- package/lib/schema/storage.js +75 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/index.js +34 -0
- package/lib/utils/content-types.js +41 -0
- package/package.json +39 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const types = require('../../types');
|
|
6
|
+
const { toColumnName } = require('./transform');
|
|
7
|
+
|
|
8
|
+
const applySearch = (knex, query, ctx) => {
|
|
9
|
+
const { qb, uid, db } = ctx;
|
|
10
|
+
const meta = db.metadata.get(uid);
|
|
11
|
+
|
|
12
|
+
const { attributes } = meta;
|
|
13
|
+
|
|
14
|
+
const searchColumns = ['id'];
|
|
15
|
+
|
|
16
|
+
const stringColumns = Object.keys(attributes).filter(attributeName => {
|
|
17
|
+
const attribute = attributes[attributeName];
|
|
18
|
+
return types.isString(attribute.type) && attribute.searchable !== false;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
searchColumns.push(...stringColumns);
|
|
22
|
+
|
|
23
|
+
if (!_.isNaN(_.toNumber(query))) {
|
|
24
|
+
const numberColumns = Object.keys(attributes).filter(attributeName => {
|
|
25
|
+
const attribute = attributes[attributeName];
|
|
26
|
+
return types.isNumber(attribute.type) && attribute.searchable !== false;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
searchColumns.push(...numberColumns);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
switch (db.dialect.client) {
|
|
33
|
+
case 'postgres': {
|
|
34
|
+
searchColumns.forEach(attr => {
|
|
35
|
+
const columnName = toColumnName(meta, attr);
|
|
36
|
+
return knex.orWhereRaw(`??::text ILIKE ?`, [
|
|
37
|
+
qb.aliasColumn(columnName),
|
|
38
|
+
`%${escapeQuery(query, '*%\\')}%`,
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case 'sqlite': {
|
|
45
|
+
searchColumns.forEach(attr => {
|
|
46
|
+
const columnName = toColumnName(meta, attr);
|
|
47
|
+
return knex.orWhereRaw(`?? LIKE ? ESCAPE '\\'`, [
|
|
48
|
+
qb.aliasColumn(columnName),
|
|
49
|
+
`%${escapeQuery(query, '*%\\')}%`,
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case 'mysql': {
|
|
55
|
+
searchColumns.forEach(attr => {
|
|
56
|
+
const columnName = toColumnName(meta, attr);
|
|
57
|
+
return knex.orWhereRaw(`?? LIKE ?`, [
|
|
58
|
+
qb.aliasColumn(columnName),
|
|
59
|
+
`%${escapeQuery(query, '*%\\')}%`,
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
default: {
|
|
65
|
+
// do nothing
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const escapeQuery = (query, charsToEscape, escapeChar = '\\') => {
|
|
71
|
+
return query
|
|
72
|
+
.split('')
|
|
73
|
+
.reduce(
|
|
74
|
+
(escapedQuery, char) =>
|
|
75
|
+
charsToEscape.includes(char)
|
|
76
|
+
? `${escapedQuery}${escapeChar}${char}`
|
|
77
|
+
: `${escapedQuery}${char}`,
|
|
78
|
+
''
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
applySearch,
|
|
84
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const types = require('../../types');
|
|
6
|
+
const { createField } = require('../../fields');
|
|
7
|
+
|
|
8
|
+
const fromRow = (meta, row) => {
|
|
9
|
+
if (Array.isArray(row)) {
|
|
10
|
+
return row.map(singleRow => fromRow(meta, singleRow));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { attributes } = meta;
|
|
14
|
+
|
|
15
|
+
if (_.isNil(row)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const obj = {};
|
|
20
|
+
|
|
21
|
+
for (const column in row) {
|
|
22
|
+
if (!_.has(column, meta.columnToAttribute)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const attributeName = meta.columnToAttribute[column];
|
|
27
|
+
const attribute = attributes[attributeName];
|
|
28
|
+
|
|
29
|
+
if (types.isScalar(attribute.type)) {
|
|
30
|
+
const field = createField(attribute);
|
|
31
|
+
|
|
32
|
+
const val = row[column] === null ? null : field.fromDB(row[column]);
|
|
33
|
+
|
|
34
|
+
obj[attributeName] = val;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (types.isRelation(attribute.type)) {
|
|
38
|
+
obj[attributeName] = row[column];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return obj;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const toRow = (meta, data = {}) => {
|
|
46
|
+
if (_.isNil(data)) {
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (_.isArray(data)) {
|
|
51
|
+
return data.map(datum => toRow(meta, datum));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { attributes } = meta;
|
|
55
|
+
|
|
56
|
+
for (const key in data) {
|
|
57
|
+
const attribute = attributes[key];
|
|
58
|
+
|
|
59
|
+
if (!attribute || attribute.columnName === key) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
data[attribute.columnName] = data[key];
|
|
64
|
+
delete data[key];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return data;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const toColumnName = (meta, name) => {
|
|
71
|
+
const attribute = meta.attributes[name];
|
|
72
|
+
|
|
73
|
+
if (!attribute) {
|
|
74
|
+
return name;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return attribute.columnName || name;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
toRow,
|
|
82
|
+
fromRow,
|
|
83
|
+
toColumnName,
|
|
84
|
+
};
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const types = require('../../types');
|
|
6
|
+
const { createJoin } = require('./join');
|
|
7
|
+
const { toColumnName } = require('./transform');
|
|
8
|
+
|
|
9
|
+
const GROUP_OPERATORS = ['$and', '$or'];
|
|
10
|
+
const OPERATORS = [
|
|
11
|
+
'$not',
|
|
12
|
+
'$in',
|
|
13
|
+
'$notIn',
|
|
14
|
+
'$eq',
|
|
15
|
+
'$ne',
|
|
16
|
+
'$gt',
|
|
17
|
+
'$gte',
|
|
18
|
+
'$lt',
|
|
19
|
+
'$lte',
|
|
20
|
+
'$null',
|
|
21
|
+
'$notNull',
|
|
22
|
+
'$between',
|
|
23
|
+
'$startsWith',
|
|
24
|
+
'$endsWith',
|
|
25
|
+
'$contains',
|
|
26
|
+
'$notContains',
|
|
27
|
+
'$containsi',
|
|
28
|
+
'$notContainsi',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const ARRAY_OPERATORS = ['$in', '$notIn', '$between'];
|
|
32
|
+
|
|
33
|
+
const isOperator = key => OPERATORS.includes(key);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Process where parameter
|
|
37
|
+
* @param {Object} where
|
|
38
|
+
* @param {Object} ctx
|
|
39
|
+
* @param {number} depth
|
|
40
|
+
* @returns {Object}
|
|
41
|
+
*/
|
|
42
|
+
const processWhere = (where, ctx, depth = 0) => {
|
|
43
|
+
if (!_.isArray(where) && !_.isPlainObject(where)) {
|
|
44
|
+
throw new Error('Where must be an array or an object');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (_.isArray(where)) {
|
|
48
|
+
return where.map(sub => processWhere(sub, ctx));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const processNested = (where, ctx) => {
|
|
52
|
+
if (!_.isPlainObject(where)) {
|
|
53
|
+
return where;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return processWhere(where, ctx, depth + 1);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const { db, uid, qb, alias } = ctx;
|
|
60
|
+
const meta = db.metadata.get(uid);
|
|
61
|
+
|
|
62
|
+
const filters = {};
|
|
63
|
+
|
|
64
|
+
// for each key in where
|
|
65
|
+
for (const key in where) {
|
|
66
|
+
const value = where[key];
|
|
67
|
+
const attribute = meta.attributes[key];
|
|
68
|
+
|
|
69
|
+
// if operator $and $or then loop over them
|
|
70
|
+
if (GROUP_OPERATORS.includes(key)) {
|
|
71
|
+
filters[key] = value.map(sub => processNested(sub, ctx));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (key === '$not') {
|
|
76
|
+
filters[key] = processNested(value, ctx);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isOperator(key)) {
|
|
81
|
+
if (depth == 0) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Only $and, $or and $not can by used as root level operators. Found ${key}.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
filters[key] = processNested(value, ctx);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!attribute) {
|
|
92
|
+
filters[qb.aliasColumn(key, alias)] = processNested(value, ctx);
|
|
93
|
+
|
|
94
|
+
continue;
|
|
95
|
+
|
|
96
|
+
// throw new Error(`Attribute ${key} not found on model ${uid}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// move to if else to check for scalar / relation / components & throw for other types
|
|
100
|
+
if (attribute.type === 'relation') {
|
|
101
|
+
// TODO: pass down some filters (e.g published at)
|
|
102
|
+
|
|
103
|
+
// attribute
|
|
104
|
+
const subAlias = createJoin(ctx, {
|
|
105
|
+
alias: alias || qb.alias,
|
|
106
|
+
uid,
|
|
107
|
+
attributeName: key,
|
|
108
|
+
attribute,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
let nestedWhere = processNested(value, {
|
|
112
|
+
db,
|
|
113
|
+
qb,
|
|
114
|
+
alias: subAlias,
|
|
115
|
+
uid: attribute.target,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!_.isPlainObject(nestedWhere) || isOperator(_.keys(nestedWhere)[0])) {
|
|
119
|
+
nestedWhere = { [qb.aliasColumn('id', subAlias)]: nestedWhere };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// TODO: use a better merge logic (push to $and when collisions)
|
|
123
|
+
Object.assign(filters, nestedWhere);
|
|
124
|
+
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (types.isScalar(attribute.type)) {
|
|
129
|
+
const columnName = toColumnName(meta, key);
|
|
130
|
+
|
|
131
|
+
// TODO: cast to DB type
|
|
132
|
+
filters[qb.aliasColumn(columnName, alias)] = processNested(value, ctx);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw new Error(`You cannot filter on ${attribute.type} types`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return filters;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const applyOperator = (qb, column, operator, value) => {
|
|
143
|
+
if (Array.isArray(value) && !ARRAY_OPERATORS.includes(operator)) {
|
|
144
|
+
return qb.where(subQB => {
|
|
145
|
+
value.forEach(subValue =>
|
|
146
|
+
subQB.orWhere(innerQB => {
|
|
147
|
+
applyOperator(innerQB, column, operator, subValue);
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
switch (operator) {
|
|
154
|
+
case '$not': {
|
|
155
|
+
qb.whereNot(qb => applyWhereToColumn(qb, column, value));
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case '$in': {
|
|
160
|
+
qb.whereIn(column, _.castArray(value));
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
case '$notIn': {
|
|
165
|
+
qb.whereNotIn(column, _.castArray(value));
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
case '$eq': {
|
|
170
|
+
if (value === null) {
|
|
171
|
+
qb.whereNull(column);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
qb.where(column, value);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case '$ne': {
|
|
179
|
+
if (value === null) {
|
|
180
|
+
qb.whereNotNull(column);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
qb.where(column, '<>', value);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case '$gt': {
|
|
188
|
+
qb.where(column, '>', value);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case '$gte': {
|
|
192
|
+
qb.where(column, '>=', value);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case '$lt': {
|
|
196
|
+
qb.where(column, '<', value);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case '$lte': {
|
|
200
|
+
qb.where(column, '<=', value);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case '$null': {
|
|
204
|
+
// TODO: make this better
|
|
205
|
+
if (value) {
|
|
206
|
+
qb.whereNull(column);
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case '$notNull': {
|
|
211
|
+
if (value) {
|
|
212
|
+
qb.whereNotNull(column);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case '$between': {
|
|
218
|
+
qb.whereBetween(column, value);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// TODO: add casting logic
|
|
223
|
+
case '$startsWith': {
|
|
224
|
+
qb.where(column, 'like', `${value}%`);
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case '$endsWith': {
|
|
228
|
+
qb.where(column, 'like', `%${value}`);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case '$contains': {
|
|
232
|
+
qb.where(column, 'like', `%${value}%`);
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
case '$notContains': {
|
|
237
|
+
qb.whereNot(column, 'like', `%${value}%`);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case '$containsi': {
|
|
242
|
+
qb.whereRaw(`${fieldLowerFn(qb)} LIKE LOWER(?)`, [column, `%${value}%`]);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
case '$notContainsi': {
|
|
247
|
+
qb.whereRaw(`${fieldLowerFn(qb)} NOT LIKE LOWER(?)`, [column, `%${value}%`]);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// TODO: json operators
|
|
252
|
+
|
|
253
|
+
// TODO: relational operators every/some/exists/size ...
|
|
254
|
+
|
|
255
|
+
default: {
|
|
256
|
+
throw new Error(`Undefined operator ${operator}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const applyWhereToColumn = (qb, column, columnWhere) => {
|
|
262
|
+
if (!_.isPlainObject(columnWhere)) {
|
|
263
|
+
if (Array.isArray(columnWhere)) {
|
|
264
|
+
return qb.whereIn(column, columnWhere);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return qb.where(column, columnWhere);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// TODO: handle casing
|
|
271
|
+
Object.keys(columnWhere).forEach(operator => {
|
|
272
|
+
const value = columnWhere[operator];
|
|
273
|
+
|
|
274
|
+
applyOperator(qb, column, operator, value);
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const applyWhere = (qb, where) => {
|
|
279
|
+
if (!_.isArray(where) && !_.isPlainObject(where)) {
|
|
280
|
+
throw new Error('Where must be an array or an object');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (_.isArray(where)) {
|
|
284
|
+
return qb.where(subQB => where.forEach(subWhere => applyWhere(subQB, subWhere)));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
Object.keys(where).forEach(key => {
|
|
288
|
+
const value = where[key];
|
|
289
|
+
|
|
290
|
+
if (key === '$and') {
|
|
291
|
+
return qb.where(subQB => {
|
|
292
|
+
value.forEach(v => applyWhere(subQB, v));
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (key === '$or') {
|
|
297
|
+
return qb.where(subQB => {
|
|
298
|
+
value.forEach(v => subQB.orWhere(inner => applyWhere(inner, v)));
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (key === '$not') {
|
|
303
|
+
return qb.whereNot(qb => applyWhere(qb, value));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
applyWhereToColumn(qb, key, value);
|
|
307
|
+
});
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const fieldLowerFn = qb => {
|
|
311
|
+
// Postgres requires string to be passed
|
|
312
|
+
if (qb.client.config.client === 'postgres') {
|
|
313
|
+
return 'LOWER(CAST(?? AS VARCHAR))';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return 'LOWER(??)';
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
module.exports = {
|
|
320
|
+
applyWhere,
|
|
321
|
+
processWhere,
|
|
322
|
+
};
|