@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.
Files changed (52) hide show
  1. package/LICENSE +22 -0
  2. package/examples/connections.js +36 -0
  3. package/examples/data.sqlite +0 -0
  4. package/examples/docker-compose.yml +29 -0
  5. package/examples/index.js +73 -0
  6. package/examples/models.js +341 -0
  7. package/examples/typings.ts +17 -0
  8. package/lib/dialects/dialect.js +45 -0
  9. package/lib/dialects/index.js +28 -0
  10. package/lib/dialects/mysql/index.js +51 -0
  11. package/lib/dialects/mysql/schema-inspector.js +203 -0
  12. package/lib/dialects/postgresql/index.js +49 -0
  13. package/lib/dialects/postgresql/schema-inspector.js +229 -0
  14. package/lib/dialects/sqlite/index.js +74 -0
  15. package/lib/dialects/sqlite/schema-inspector.js +151 -0
  16. package/lib/entity-manager.js +886 -0
  17. package/lib/entity-repository.js +110 -0
  18. package/lib/errors.js +14 -0
  19. package/lib/fields.d.ts +9 -0
  20. package/lib/fields.js +232 -0
  21. package/lib/index.d.ts +146 -0
  22. package/lib/index.js +60 -0
  23. package/lib/lifecycles/index.d.ts +50 -0
  24. package/lib/lifecycles/index.js +66 -0
  25. package/lib/lifecycles/subscribers/index.d.ts +9 -0
  26. package/lib/lifecycles/subscribers/models-lifecycles.js +19 -0
  27. package/lib/lifecycles/subscribers/timestamps.js +65 -0
  28. package/lib/metadata/index.js +219 -0
  29. package/lib/metadata/relations.js +488 -0
  30. package/lib/migrations/index.d.ts +9 -0
  31. package/lib/migrations/index.js +69 -0
  32. package/lib/migrations/storage.js +49 -0
  33. package/lib/query/helpers/index.js +10 -0
  34. package/lib/query/helpers/join.js +95 -0
  35. package/lib/query/helpers/order-by.js +70 -0
  36. package/lib/query/helpers/populate.js +652 -0
  37. package/lib/query/helpers/search.js +84 -0
  38. package/lib/query/helpers/transform.js +84 -0
  39. package/lib/query/helpers/where.js +322 -0
  40. package/lib/query/index.js +7 -0
  41. package/lib/query/query-builder.js +348 -0
  42. package/lib/schema/__tests__/schema-diff.test.js +181 -0
  43. package/lib/schema/builder.js +352 -0
  44. package/lib/schema/diff.js +376 -0
  45. package/lib/schema/index.d.ts +49 -0
  46. package/lib/schema/index.js +95 -0
  47. package/lib/schema/schema.js +209 -0
  48. package/lib/schema/storage.js +75 -0
  49. package/lib/types/index.d.ts +6 -0
  50. package/lib/types/index.js +34 -0
  51. package/lib/utils/content-types.js +41 -0
  52. 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
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const createQueryBuilder = require('./query-builder');
4
+
5
+ module.exports = {
6
+ createQueryBuilder,
7
+ };