@strapi/database 4.0.0-beta.3 → 4.0.0-beta.31
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/dialects/mysql/schema-inspector.js +0 -4
- package/lib/dialects/postgresql/schema-inspector.js +17 -14
- package/lib/entity-manager.js +3 -4
- package/lib/errors.js +44 -2
- package/lib/fields.js +7 -16
- package/lib/index.js +31 -2
- package/lib/migrations/index.js +1 -1
- package/lib/migrations/storage.js +9 -7
- package/lib/query/helpers/join.js +1 -1
- package/lib/query/helpers/populate.js +6 -1
- package/lib/query/helpers/where.js +68 -27
- package/lib/query/query-builder.js +30 -5
- package/lib/schema/builder.js +45 -22
- package/lib/schema/storage.js +14 -10
- package/package.json +2 -2
- package/examples/data.sqlite +0 -0
|
@@ -123,10 +123,6 @@ class MysqlSchemaInspector {
|
|
|
123
123
|
return schema;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
getDatabaseSchema() {
|
|
127
|
-
return this.db.connection.client.connectionSettings.schema || 'public';
|
|
128
|
-
}
|
|
129
|
-
|
|
130
126
|
async getTables() {
|
|
131
127
|
const [rows] = await this.db.connection.raw(SQL_QUERIES.TABLE_LIST);
|
|
132
128
|
|
|
@@ -4,36 +4,39 @@ const SQL_QUERIES = {
|
|
|
4
4
|
TABLE_LIST: /* sql */ `
|
|
5
5
|
SELECT *
|
|
6
6
|
FROM information_schema.tables
|
|
7
|
-
WHERE
|
|
7
|
+
WHERE
|
|
8
|
+
table_schema = ?
|
|
9
|
+
AND table_type = 'BASE TABLE'
|
|
10
|
+
AND table_name != 'geometry_columns'
|
|
11
|
+
AND table_name != 'spatial_ref_sys';
|
|
8
12
|
`,
|
|
9
13
|
LIST_COLUMNS: /* sql */ `
|
|
10
14
|
SELECT data_type, column_name, character_maximum_length, column_default, is_nullable
|
|
11
15
|
FROM information_schema.columns
|
|
12
|
-
WHERE table_schema = ?
|
|
13
|
-
AND table_name = ?;
|
|
16
|
+
WHERE table_schema = ? AND table_name = ?;
|
|
14
17
|
`,
|
|
15
18
|
INDEX_LIST: /* sql */ `
|
|
16
|
-
|
|
19
|
+
SELECT
|
|
17
20
|
ix.indexrelid,
|
|
18
21
|
i.relname as index_name,
|
|
19
22
|
a.attname as column_name,
|
|
20
23
|
ix.indisunique as is_unique,
|
|
21
24
|
ix.indisprimary as is_primary
|
|
22
|
-
|
|
25
|
+
FROM
|
|
23
26
|
pg_class t,
|
|
24
27
|
pg_namespace s,
|
|
25
28
|
pg_class i,
|
|
26
29
|
pg_index ix,
|
|
27
30
|
pg_attribute a
|
|
28
|
-
|
|
31
|
+
WHERE
|
|
29
32
|
t.oid = ix.indrelid
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
AND i.oid = ix.indexrelid
|
|
34
|
+
AND a.attrelid = t.oid
|
|
35
|
+
AND a.attnum = ANY(ix.indkey)
|
|
36
|
+
AND t.relkind = 'r'
|
|
37
|
+
AND t.relnamespace = s.oid
|
|
38
|
+
AND s.nspname = ?
|
|
39
|
+
AND t.relname = ?;
|
|
37
40
|
`,
|
|
38
41
|
FOREIGN_KEY_LIST: /* sql */ `
|
|
39
42
|
SELECT
|
|
@@ -136,7 +139,7 @@ class PostgresqlSchemaInspector {
|
|
|
136
139
|
}
|
|
137
140
|
|
|
138
141
|
getDatabaseSchema() {
|
|
139
|
-
return this.db.connection.
|
|
142
|
+
return this.db.connection.getSchemaName() || 'public';
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
async getTables() {
|
package/lib/entity-manager.js
CHANGED
|
@@ -142,7 +142,7 @@ const createEntityManager = db => {
|
|
|
142
142
|
await db.lifecycles.run('beforeCount', uid, { params });
|
|
143
143
|
|
|
144
144
|
const res = await this.createQueryBuilder(uid)
|
|
145
|
-
.init(_.pick(['_q', 'where'], params))
|
|
145
|
+
.init(_.pick(['_q', 'where', 'filters'], params))
|
|
146
146
|
.count()
|
|
147
147
|
.first()
|
|
148
148
|
.execute();
|
|
@@ -172,7 +172,7 @@ const createEntityManager = db => {
|
|
|
172
172
|
|
|
173
173
|
await this.attachRelations(uid, id, data);
|
|
174
174
|
|
|
175
|
-
// TODO: in case there is
|
|
175
|
+
// TODO: in case there is no select or populate specified return the inserted data ?
|
|
176
176
|
// TODO: do not trigger the findOne lifecycles ?
|
|
177
177
|
const result = await this.findOne(uid, {
|
|
178
178
|
where: { id },
|
|
@@ -296,7 +296,7 @@ const createEntityManager = db => {
|
|
|
296
296
|
throw new Error('Delete requires a where parameter');
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
// TODO:
|
|
299
|
+
// TODO: do not trigger the findOne lifecycles ?
|
|
300
300
|
const entity = await this.findOne(uid, {
|
|
301
301
|
select: select && ['id'].concat(select),
|
|
302
302
|
where,
|
|
@@ -469,7 +469,6 @@ const createEntityManager = db => {
|
|
|
469
469
|
const { joinTable } = attribute;
|
|
470
470
|
const { joinColumn, inverseJoinColumn } = joinTable;
|
|
471
471
|
|
|
472
|
-
// TODO: validate logic of delete
|
|
473
472
|
if (isOneToAny(attribute) && isBidirectional(attribute)) {
|
|
474
473
|
await this.createQueryBuilder(joinTable.name)
|
|
475
474
|
.delete()
|
package/lib/errors.js
CHANGED
|
@@ -1,14 +1,56 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/* DatabaseError */
|
|
4
|
+
class DatabaseError extends Error {
|
|
5
|
+
constructor(message, details = {}) {
|
|
6
|
+
super();
|
|
7
|
+
this.name = 'DatabaseError';
|
|
8
|
+
this.message = message || 'A database error occured';
|
|
9
|
+
this.details = details;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class NotNullConstraint extends DatabaseError {
|
|
4
14
|
constructor({ column = '' } = {}) {
|
|
5
15
|
super();
|
|
6
16
|
this.name = 'NotNullConstraint';
|
|
7
|
-
this.message = `Not null constraint violation${column ? `
|
|
17
|
+
this.message = `Not null constraint violation${column ? ` on column ${column}` : ''}.`;
|
|
18
|
+
this.details = { column };
|
|
8
19
|
this.stack = '';
|
|
9
20
|
}
|
|
10
21
|
}
|
|
11
22
|
|
|
23
|
+
class InvalidTimeError extends DatabaseError {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super();
|
|
26
|
+
this.name = 'InvalidTimeFormat';
|
|
27
|
+
this.message = message || 'Invalid time format, expected HH:mm:ss.SSS';
|
|
28
|
+
this.details = {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class InvalidDateError extends DatabaseError {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super();
|
|
35
|
+
this.name = 'InvalidTimeFormat';
|
|
36
|
+
this.message = message || 'Invalid date format, expected YYYY-MM-DD';
|
|
37
|
+
this.details = {};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class InvalidDateTimeError extends DatabaseError {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super();
|
|
44
|
+
this.name = 'InvalidTimeFormat';
|
|
45
|
+
this.message = message || 'Invalid datetime format, expected a timestamp or an ISO date';
|
|
46
|
+
this.details = {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
12
50
|
module.exports = {
|
|
51
|
+
DatabaseError,
|
|
13
52
|
NotNullConstraint,
|
|
53
|
+
InvalidTimeError,
|
|
54
|
+
InvalidDateError,
|
|
55
|
+
InvalidDateTimeError,
|
|
14
56
|
};
|
package/lib/fields.js
CHANGED
|
@@ -2,22 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const _ = require('lodash/fp');
|
|
4
4
|
const dateFns = require('date-fns');
|
|
5
|
+
const { InvalidTimeError, InvalidDateError, InvalidDateTimeError } = require('./errors');
|
|
5
6
|
|
|
6
7
|
class Field {
|
|
7
8
|
constructor(config) {
|
|
8
9
|
this.config = config;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
// TODO: impl
|
|
12
|
-
validate() {
|
|
13
|
-
// // use config validators directly
|
|
14
|
-
// if (this.config.validators) {
|
|
15
|
-
// this.config.validators.forEach(validator => {
|
|
16
|
-
// validator(value)
|
|
17
|
-
// })
|
|
18
|
-
// }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
12
|
toDB(value) {
|
|
22
13
|
return value;
|
|
23
14
|
}
|
|
@@ -112,12 +103,12 @@ const parseTime = value => {
|
|
|
112
103
|
if (dateFns.isDate(value)) return dateFns.format(value, 'HH:mm:ss.SSS');
|
|
113
104
|
|
|
114
105
|
if (typeof value !== 'string') {
|
|
115
|
-
throw new
|
|
106
|
+
throw new InvalidTimeError(`Expected a string, got a ${typeof value}`);
|
|
116
107
|
}
|
|
117
108
|
const result = value.match(timeRegex);
|
|
118
109
|
|
|
119
110
|
if (result === null) {
|
|
120
|
-
throw new
|
|
111
|
+
throw new InvalidTimeError('Invalid time format, expected HH:mm:ss.SSS');
|
|
121
112
|
}
|
|
122
113
|
|
|
123
114
|
const [, hours, minutes, seconds, fraction = '.000'] = result;
|
|
@@ -133,9 +124,9 @@ const parseDate = value => {
|
|
|
133
124
|
|
|
134
125
|
if (dateFns.isValid(date)) return dateFns.format(date, 'yyyy-MM-dd');
|
|
135
126
|
|
|
136
|
-
throw new
|
|
127
|
+
throw new InvalidDateError(`Invalid format, expected an ISO compatible date`);
|
|
137
128
|
} catch (error) {
|
|
138
|
-
throw new
|
|
129
|
+
throw new InvalidDateError(`Invalid format, expected an ISO compatible date`);
|
|
139
130
|
}
|
|
140
131
|
};
|
|
141
132
|
|
|
@@ -148,9 +139,9 @@ const parseDateTimeOrTimestamp = value => {
|
|
|
148
139
|
const milliUnixDate = dateFns.parse(value, 'T', new Date());
|
|
149
140
|
if (dateFns.isValid(milliUnixDate)) return milliUnixDate;
|
|
150
141
|
|
|
151
|
-
throw new
|
|
142
|
+
throw new InvalidDateTimeError(`Invalid format, expected a timestamp or an ISO date`);
|
|
152
143
|
} catch (error) {
|
|
153
|
-
throw new
|
|
144
|
+
throw new InvalidDateTimeError(`Invalid format, expected a timestamp or an ISO date`);
|
|
154
145
|
}
|
|
155
146
|
};
|
|
156
147
|
|
package/lib/index.js
CHANGED
|
@@ -8,20 +8,37 @@ const createMetadata = require('./metadata');
|
|
|
8
8
|
const { createEntityManager } = require('./entity-manager');
|
|
9
9
|
const { createMigrationsProvider } = require('./migrations');
|
|
10
10
|
const { createLifecyclesProvider } = require('./lifecycles');
|
|
11
|
+
const errors = require('./errors');
|
|
11
12
|
|
|
12
13
|
// TODO: move back into strapi
|
|
13
14
|
const { transformContentTypes } = require('./utils/content-types');
|
|
14
15
|
|
|
16
|
+
const createConnection = config => {
|
|
17
|
+
const knexInstance = knex(config);
|
|
18
|
+
|
|
19
|
+
return Object.assign(knexInstance, {
|
|
20
|
+
getSchemaName() {
|
|
21
|
+
return this.client.connectionSettings.schema;
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
15
26
|
class Database {
|
|
16
27
|
constructor(config) {
|
|
17
28
|
this.metadata = createMetadata(config.models);
|
|
18
29
|
|
|
19
|
-
this.config =
|
|
30
|
+
this.config = {
|
|
31
|
+
connection: {},
|
|
32
|
+
settings: {
|
|
33
|
+
forceMigration: true,
|
|
34
|
+
},
|
|
35
|
+
...config,
|
|
36
|
+
};
|
|
20
37
|
|
|
21
38
|
this.dialect = getDialect(this);
|
|
22
39
|
this.dialect.configure();
|
|
23
40
|
|
|
24
|
-
this.connection =
|
|
41
|
+
this.connection = createConnection(this.config.connection);
|
|
25
42
|
|
|
26
43
|
this.dialect.initialize();
|
|
27
44
|
|
|
@@ -41,6 +58,17 @@ class Database {
|
|
|
41
58
|
return this.entityManager.getRepository(uid);
|
|
42
59
|
}
|
|
43
60
|
|
|
61
|
+
getConnection(tableName) {
|
|
62
|
+
const schema = this.connection.getSchemaName();
|
|
63
|
+
const connection = tableName ? this.connection(tableName) : this.connection;
|
|
64
|
+
return schema ? connection.withSchema(schema) : connection;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getSchemaConnection(trx = this.connection) {
|
|
68
|
+
const schema = this.connection.getSchemaName();
|
|
69
|
+
return schema ? trx.schema.withSchema(schema) : trx.schema;
|
|
70
|
+
}
|
|
71
|
+
|
|
44
72
|
queryBuilder(uid) {
|
|
45
73
|
return this.entityManager.createQueryBuilder(uid);
|
|
46
74
|
}
|
|
@@ -57,4 +85,5 @@ Database.init = async config => new Database(config);
|
|
|
57
85
|
|
|
58
86
|
module.exports = {
|
|
59
87
|
Database,
|
|
88
|
+
errors,
|
|
60
89
|
};
|
package/lib/migrations/index.js
CHANGED
|
@@ -27,7 +27,7 @@ const createUmzugProvider = db => {
|
|
|
27
27
|
|
|
28
28
|
fse.ensureDirSync(migrationDir);
|
|
29
29
|
|
|
30
|
-
const wrapFn = fn => db => db.
|
|
30
|
+
const wrapFn = fn => db => db.getConnection().transaction(trx => Promise.resolve(fn(trx)));
|
|
31
31
|
const storage = createStorage({ db, tableName: 'strapi_migrations' });
|
|
32
32
|
|
|
33
33
|
return new Umzug({
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const createStorage = (opts = {}) => {
|
|
4
|
-
const
|
|
5
|
-
const knex = opts.db.connection;
|
|
4
|
+
const { db, tableName = 'strapi_migrations' } = opts;
|
|
6
5
|
|
|
7
|
-
const hasMigrationTable = () =>
|
|
6
|
+
const hasMigrationTable = () => db.getSchemaConnection().hasTable(tableName);
|
|
8
7
|
|
|
9
8
|
const createMigrationTable = () => {
|
|
10
|
-
return
|
|
9
|
+
return db.getSchemaConnection().createTable(tableName, table => {
|
|
11
10
|
table.increments('id');
|
|
12
11
|
table.string('name');
|
|
13
12
|
table.datetime('time', { useTz: false });
|
|
@@ -16,7 +15,8 @@ const createStorage = (opts = {}) => {
|
|
|
16
15
|
|
|
17
16
|
return {
|
|
18
17
|
async logMigration(migrationName) {
|
|
19
|
-
await
|
|
18
|
+
await db
|
|
19
|
+
.getConnection()
|
|
20
20
|
.insert({
|
|
21
21
|
name: migrationName,
|
|
22
22
|
time: new Date(),
|
|
@@ -25,7 +25,8 @@ const createStorage = (opts = {}) => {
|
|
|
25
25
|
},
|
|
26
26
|
|
|
27
27
|
async unlogMigration(migrationName) {
|
|
28
|
-
await
|
|
28
|
+
await db
|
|
29
|
+
.getConnection(tableName)
|
|
29
30
|
.del()
|
|
30
31
|
.where({ name: migrationName });
|
|
31
32
|
},
|
|
@@ -36,7 +37,8 @@ const createStorage = (opts = {}) => {
|
|
|
36
37
|
return [];
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
const logs = await
|
|
40
|
+
const logs = await db
|
|
41
|
+
.getConnection(tableName)
|
|
40
42
|
.select()
|
|
41
43
|
.from(tableName)
|
|
42
44
|
.orderBy('time');
|
|
@@ -67,7 +67,7 @@ const applyJoin = (qb, join) => {
|
|
|
67
67
|
orderBy,
|
|
68
68
|
} = join;
|
|
69
69
|
|
|
70
|
-
qb[method]({
|
|
70
|
+
qb[method](`${referencedTable} as ${alias}`, inner => {
|
|
71
71
|
inner.on(`${rootTable}.${rootColumn}`, `${alias}.${referencedColumn}`);
|
|
72
72
|
|
|
73
73
|
if (on) {
|
|
@@ -103,6 +103,7 @@ const pickPopulateParams = _.pick([
|
|
|
103
103
|
'orderBy',
|
|
104
104
|
'limit',
|
|
105
105
|
'offset',
|
|
106
|
+
'filters',
|
|
106
107
|
]);
|
|
107
108
|
|
|
108
109
|
// TODO: cleanup code
|
|
@@ -122,7 +123,11 @@ const applyPopulate = async (results, populate, ctx) => {
|
|
|
122
123
|
const attribute = meta.attributes[key];
|
|
123
124
|
const targetMeta = db.metadata.get(attribute.target);
|
|
124
125
|
|
|
125
|
-
const populateValue =
|
|
126
|
+
const populateValue = {
|
|
127
|
+
filters: qb.state.filters,
|
|
128
|
+
...pickPopulateParams(populate[key]),
|
|
129
|
+
};
|
|
130
|
+
|
|
126
131
|
const isCount = populateValue.count === true;
|
|
127
132
|
|
|
128
133
|
const fromTargetRow = rowOrRows => fromRow(targetMeta, rowOrRows);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const _ = require('lodash/fp');
|
|
4
4
|
|
|
5
5
|
const types = require('../../types');
|
|
6
|
+
const { createField } = require('../../fields');
|
|
6
7
|
const { createJoin } = require('./join');
|
|
7
8
|
const { toColumnName } = require('./transform');
|
|
8
9
|
|
|
@@ -28,10 +29,65 @@ const OPERATORS = [
|
|
|
28
29
|
'$notContainsi',
|
|
29
30
|
];
|
|
30
31
|
|
|
32
|
+
const CAST_OPERATORS = [
|
|
33
|
+
'$not',
|
|
34
|
+
'$in',
|
|
35
|
+
'$notIn',
|
|
36
|
+
'$eq',
|
|
37
|
+
'$ne',
|
|
38
|
+
'$gt',
|
|
39
|
+
'$gte',
|
|
40
|
+
'$lt',
|
|
41
|
+
'$lte',
|
|
42
|
+
'$between',
|
|
43
|
+
];
|
|
44
|
+
|
|
31
45
|
const ARRAY_OPERATORS = ['$in', '$notIn', '$between'];
|
|
32
46
|
|
|
33
47
|
const isOperator = key => OPERATORS.includes(key);
|
|
34
48
|
|
|
49
|
+
const castValue = (value, attribute) => {
|
|
50
|
+
if (!attribute) {
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (types.isScalar(attribute.type)) {
|
|
55
|
+
const field = createField(attribute);
|
|
56
|
+
|
|
57
|
+
return value === null ? null : field.toDB(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return value;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const processAttributeWhere = (attribute, where, operator = '$eq') => {
|
|
64
|
+
if (_.isArray(where)) {
|
|
65
|
+
return where.map(sub => processAttributeWhere(attribute, sub, operator));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!_.isPlainObject(where)) {
|
|
69
|
+
if (CAST_OPERATORS.includes(operator)) {
|
|
70
|
+
return castValue(where, attribute);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return where;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const filters = {};
|
|
77
|
+
|
|
78
|
+
for (const key in where) {
|
|
79
|
+
const value = where[key];
|
|
80
|
+
|
|
81
|
+
if (!isOperator(key)) {
|
|
82
|
+
throw new Error(`Undefined attribute level operator ${key}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
filters[key] = processAttributeWhere(attribute, value, key);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return filters;
|
|
89
|
+
};
|
|
90
|
+
|
|
35
91
|
/**
|
|
36
92
|
* Process where parameter
|
|
37
93
|
* @param {Object} where
|
|
@@ -39,7 +95,7 @@ const isOperator = key => OPERATORS.includes(key);
|
|
|
39
95
|
* @param {number} depth
|
|
40
96
|
* @returns {Object}
|
|
41
97
|
*/
|
|
42
|
-
const processWhere = (where, ctx
|
|
98
|
+
const processWhere = (where, ctx) => {
|
|
43
99
|
if (!_.isArray(where) && !_.isPlainObject(where)) {
|
|
44
100
|
throw new Error('Where must be an array or an object');
|
|
45
101
|
}
|
|
@@ -53,7 +109,7 @@ const processWhere = (where, ctx, depth = 0) => {
|
|
|
53
109
|
return where;
|
|
54
110
|
}
|
|
55
111
|
|
|
56
|
-
return processWhere(where, ctx
|
|
112
|
+
return processWhere(where, ctx);
|
|
57
113
|
};
|
|
58
114
|
|
|
59
115
|
const { db, uid, qb, alias } = ctx;
|
|
@@ -64,7 +120,6 @@ const processWhere = (where, ctx, depth = 0) => {
|
|
|
64
120
|
// for each key in where
|
|
65
121
|
for (const key in where) {
|
|
66
122
|
const value = where[key];
|
|
67
|
-
const attribute = meta.attributes[key];
|
|
68
123
|
|
|
69
124
|
// if operator $and $or then loop over them
|
|
70
125
|
if (GROUP_OPERATORS.includes(key)) {
|
|
@@ -78,28 +133,17 @@ const processWhere = (where, ctx, depth = 0) => {
|
|
|
78
133
|
}
|
|
79
134
|
|
|
80
135
|
if (isOperator(key)) {
|
|
81
|
-
|
|
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;
|
|
136
|
+
throw new Error(`Only $and, $or and $not can by used as root level operators. Found ${key}.`);
|
|
89
137
|
}
|
|
90
138
|
|
|
91
|
-
|
|
92
|
-
filters[qb.aliasColumn(key, alias)] = processNested(value, ctx);
|
|
139
|
+
const attribute = meta.attributes[key];
|
|
93
140
|
|
|
141
|
+
if (!attribute) {
|
|
142
|
+
filters[qb.aliasColumn(key, alias)] = processAttributeWhere(null, value);
|
|
94
143
|
continue;
|
|
95
|
-
|
|
96
|
-
// throw new Error(`Attribute ${key} not found on model ${uid}`);
|
|
97
144
|
}
|
|
98
145
|
|
|
99
|
-
|
|
100
|
-
if (attribute.type === 'relation') {
|
|
101
|
-
// TODO: pass down some filters (e.g published at)
|
|
102
|
-
|
|
146
|
+
if (types.isRelation(attribute.type)) {
|
|
103
147
|
// attribute
|
|
104
148
|
const subAlias = createJoin(ctx, {
|
|
105
149
|
alias: alias || qb.alias,
|
|
@@ -127,9 +171,10 @@ const processWhere = (where, ctx, depth = 0) => {
|
|
|
127
171
|
|
|
128
172
|
if (types.isScalar(attribute.type)) {
|
|
129
173
|
const columnName = toColumnName(meta, key);
|
|
174
|
+
const aliasedColumnName = qb.aliasColumn(columnName, alias);
|
|
175
|
+
|
|
176
|
+
filters[aliasedColumnName] = processAttributeWhere(attribute, value);
|
|
130
177
|
|
|
131
|
-
// TODO: cast to DB type
|
|
132
|
-
filters[qb.aliasColumn(columnName, alias)] = processNested(value, ctx);
|
|
133
178
|
continue;
|
|
134
179
|
}
|
|
135
180
|
|
|
@@ -139,6 +184,7 @@ const processWhere = (where, ctx, depth = 0) => {
|
|
|
139
184
|
return filters;
|
|
140
185
|
};
|
|
141
186
|
|
|
187
|
+
// TODO: add type casting per operator at some point
|
|
142
188
|
const applyOperator = (qb, column, operator, value) => {
|
|
143
189
|
if (Array.isArray(value) && !ARRAY_OPERATORS.includes(operator)) {
|
|
144
190
|
return qb.where(subQB => {
|
|
@@ -201,7 +247,6 @@ const applyOperator = (qb, column, operator, value) => {
|
|
|
201
247
|
break;
|
|
202
248
|
}
|
|
203
249
|
case '$null': {
|
|
204
|
-
// TODO: make this better
|
|
205
250
|
if (value) {
|
|
206
251
|
qb.whereNull(column);
|
|
207
252
|
}
|
|
@@ -211,15 +256,12 @@ const applyOperator = (qb, column, operator, value) => {
|
|
|
211
256
|
if (value) {
|
|
212
257
|
qb.whereNotNull(column);
|
|
213
258
|
}
|
|
214
|
-
|
|
215
259
|
break;
|
|
216
260
|
}
|
|
217
261
|
case '$between': {
|
|
218
262
|
qb.whereBetween(column, value);
|
|
219
263
|
break;
|
|
220
264
|
}
|
|
221
|
-
|
|
222
|
-
// TODO: add casting logic
|
|
223
265
|
case '$startsWith': {
|
|
224
266
|
qb.where(column, 'like', `${value}%`);
|
|
225
267
|
break;
|
|
@@ -253,7 +295,7 @@ const applyOperator = (qb, column, operator, value) => {
|
|
|
253
295
|
// TODO: relational operators every/some/exists/size ...
|
|
254
296
|
|
|
255
297
|
default: {
|
|
256
|
-
throw new Error(`Undefined operator ${operator}`);
|
|
298
|
+
throw new Error(`Undefined attribute level operator ${operator}`);
|
|
257
299
|
}
|
|
258
300
|
}
|
|
259
301
|
};
|
|
@@ -267,7 +309,6 @@ const applyWhereToColumn = (qb, column, columnWhere) => {
|
|
|
267
309
|
return qb.where(column, columnWhere);
|
|
268
310
|
}
|
|
269
311
|
|
|
270
|
-
// TODO: handle casing
|
|
271
312
|
Object.keys(columnWhere).forEach(operator => {
|
|
272
313
|
const value = columnWhere[operator];
|
|
273
314
|
|
|
@@ -29,6 +29,7 @@ const createQueryBuilder = (uid, db) => {
|
|
|
29
29
|
return {
|
|
30
30
|
alias: getAlias(),
|
|
31
31
|
getAlias,
|
|
32
|
+
state,
|
|
32
33
|
|
|
33
34
|
select(args) {
|
|
34
35
|
state.type = 'select';
|
|
@@ -115,7 +116,7 @@ const createQueryBuilder = (uid, db) => {
|
|
|
115
116
|
},
|
|
116
117
|
|
|
117
118
|
init(params = {}) {
|
|
118
|
-
const { _q, where, select, limit, offset, orderBy, groupBy, populate } = params;
|
|
119
|
+
const { _q, filters, where, select, limit, offset, orderBy, groupBy, populate } = params;
|
|
119
120
|
|
|
120
121
|
if (!_.isNil(where)) {
|
|
121
122
|
this.where(where);
|
|
@@ -151,9 +152,17 @@ const createQueryBuilder = (uid, db) => {
|
|
|
151
152
|
this.populate(populate);
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
if (!_.isNil(filters)) {
|
|
156
|
+
this.filters(filters);
|
|
157
|
+
}
|
|
158
|
+
|
|
154
159
|
return this;
|
|
155
160
|
},
|
|
156
161
|
|
|
162
|
+
filters(filters) {
|
|
163
|
+
state.filters = filters;
|
|
164
|
+
},
|
|
165
|
+
|
|
157
166
|
first() {
|
|
158
167
|
state.first = true;
|
|
159
168
|
return this;
|
|
@@ -196,16 +205,32 @@ const createQueryBuilder = (uid, db) => {
|
|
|
196
205
|
this.select('id');
|
|
197
206
|
const subQB = this.getKnexQuery();
|
|
198
207
|
|
|
199
|
-
const nestedSubQuery = db
|
|
208
|
+
const nestedSubQuery = db
|
|
209
|
+
.getConnection()
|
|
210
|
+
.select('id')
|
|
211
|
+
.from(subQB.as('subQuery'));
|
|
200
212
|
|
|
201
213
|
return db
|
|
202
|
-
.
|
|
214
|
+
.getConnection(tableName)
|
|
203
215
|
[state.type]()
|
|
204
216
|
.whereIn('id', nestedSubQuery);
|
|
205
217
|
},
|
|
206
218
|
|
|
207
219
|
processState() {
|
|
208
220
|
state.orderBy = helpers.processOrderBy(state.orderBy, { qb: this, uid, db });
|
|
221
|
+
|
|
222
|
+
if (!_.isNil(state.filters)) {
|
|
223
|
+
if (_.isFunction(state.filters)) {
|
|
224
|
+
const filters = state.filters({ qb: this, uid, meta, db });
|
|
225
|
+
|
|
226
|
+
if (!_.isNil(filters)) {
|
|
227
|
+
state.where.push(filters);
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
state.where.push(state.filters);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
209
234
|
state.where = helpers.processWhere(state.where, { qb: this, uid, db });
|
|
210
235
|
state.populate = helpers.processPopulate(state.populate, { qb: this, uid, db });
|
|
211
236
|
state.data = helpers.toRow(meta, state.data);
|
|
@@ -235,9 +260,9 @@ const createQueryBuilder = (uid, db) => {
|
|
|
235
260
|
this.select('*');
|
|
236
261
|
}
|
|
237
262
|
|
|
238
|
-
const aliasedTableName = this.mustUseAlias() ? {
|
|
263
|
+
const aliasedTableName = this.mustUseAlias() ? `${tableName} as ${this.alias}` : tableName;
|
|
239
264
|
|
|
240
|
-
const qb = db.
|
|
265
|
+
const qb = db.getConnection(aliasedTableName);
|
|
241
266
|
|
|
242
267
|
if (this.shouldUseSubQuery()) {
|
|
243
268
|
return this.runSubQuery();
|
package/lib/schema/builder.js
CHANGED
|
@@ -11,8 +11,8 @@ module.exports = db => {
|
|
|
11
11
|
* Returns a knex schema builder instance
|
|
12
12
|
* @param {string} table - table name
|
|
13
13
|
*/
|
|
14
|
-
getSchemaBuilder(
|
|
15
|
-
return
|
|
14
|
+
getSchemaBuilder(trx) {
|
|
15
|
+
return db.getSchemaConnection(trx);
|
|
16
16
|
},
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -20,10 +20,7 @@ module.exports = db => {
|
|
|
20
20
|
* @param {Schema} schema - database schema
|
|
21
21
|
*/
|
|
22
22
|
async createSchema(schema) {
|
|
23
|
-
// TODO: ensure database exists;
|
|
24
|
-
|
|
25
23
|
await db.connection.transaction(async trx => {
|
|
26
|
-
// create tables without FKs first do avoid ordering issues
|
|
27
24
|
await this.createTables(schema.tables, trx);
|
|
28
25
|
});
|
|
29
26
|
},
|
|
@@ -36,14 +33,14 @@ module.exports = db => {
|
|
|
36
33
|
async createTables(tables, trx) {
|
|
37
34
|
for (const table of tables) {
|
|
38
35
|
debug(`Creating table: ${table.name}`);
|
|
39
|
-
const schemaBuilder = this.getSchemaBuilder(
|
|
36
|
+
const schemaBuilder = this.getSchemaBuilder(trx);
|
|
40
37
|
await helpers.createTable(schemaBuilder, table);
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
// create FKs once all the tables exist
|
|
44
41
|
for (const table of tables) {
|
|
45
42
|
debug(`Creating table foreign keys: ${table.name}`);
|
|
46
|
-
const schemaBuilder = this.getSchemaBuilder(
|
|
43
|
+
const schemaBuilder = this.getSchemaBuilder(trx);
|
|
47
44
|
await helpers.createTableForeignKeys(schemaBuilder, table);
|
|
48
45
|
}
|
|
49
46
|
},
|
|
@@ -61,7 +58,7 @@ module.exports = db => {
|
|
|
61
58
|
|
|
62
59
|
await db.connection.transaction(async trx => {
|
|
63
60
|
for (const table of schema.tables.reverse()) {
|
|
64
|
-
const schemaBuilder = this.getSchemaBuilder(
|
|
61
|
+
const schemaBuilder = this.getSchemaBuilder(trx);
|
|
65
62
|
await helpers.dropTable(schemaBuilder, table);
|
|
66
63
|
}
|
|
67
64
|
});
|
|
@@ -73,29 +70,33 @@ module.exports = db => {
|
|
|
73
70
|
*/
|
|
74
71
|
// TODO: implement force option to disable removal in DB
|
|
75
72
|
async updateSchema(schemaDiff) {
|
|
73
|
+
const { forceMigration } = db.config.settings;
|
|
74
|
+
|
|
76
75
|
await db.dialect.startSchemaUpdate();
|
|
77
76
|
await db.connection.transaction(async trx => {
|
|
78
77
|
await this.createTables(schemaDiff.tables.added, trx);
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
if (forceMigration) {
|
|
80
|
+
// drop all delete table foreign keys then delete the tables
|
|
81
|
+
for (const table of schemaDiff.tables.removed) {
|
|
82
|
+
debug(`Removing table foreign keys: ${table.name}`);
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
const schemaBuilder = this.getSchemaBuilder(trx);
|
|
85
|
+
await helpers.dropTableForeignKeys(schemaBuilder, table);
|
|
86
|
+
}
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
for (const table of schemaDiff.tables.removed) {
|
|
89
|
+
debug(`Removing table: ${table.name}`);
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
const schemaBuilder = this.getSchemaBuilder(trx);
|
|
92
|
+
await helpers.dropTable(schemaBuilder, table);
|
|
93
|
+
}
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
for (const table of schemaDiff.tables.updated) {
|
|
96
97
|
debug(`Updating table: ${table.name}`);
|
|
97
98
|
// alter table
|
|
98
|
-
const schemaBuilder = this.getSchemaBuilder(
|
|
99
|
+
const schemaBuilder = this.getSchemaBuilder(trx);
|
|
99
100
|
|
|
100
101
|
await helpers.alterTable(schemaBuilder, table);
|
|
101
102
|
}
|
|
@@ -118,7 +119,11 @@ const createHelpers = db => {
|
|
|
118
119
|
const constraint = tableBuilder
|
|
119
120
|
.foreign(columns, name)
|
|
120
121
|
.references(referencedColumns)
|
|
121
|
-
.inTable(
|
|
122
|
+
.inTable(
|
|
123
|
+
db.connection.getSchemaName()
|
|
124
|
+
? `${db.connection.getSchemaName()}.${referencedTable}`
|
|
125
|
+
: referencedTable
|
|
126
|
+
);
|
|
122
127
|
|
|
123
128
|
if (onDelete) {
|
|
124
129
|
constraint.onDelete(onDelete);
|
|
@@ -167,6 +172,10 @@ const createHelpers = db => {
|
|
|
167
172
|
* @param {Index} index
|
|
168
173
|
*/
|
|
169
174
|
const dropIndex = (tableBuilder, index) => {
|
|
175
|
+
if (!db.config.settings.forceMigration) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
170
179
|
const { type, columns, name } = index;
|
|
171
180
|
|
|
172
181
|
switch (type) {
|
|
@@ -221,7 +230,11 @@ const createHelpers = db => {
|
|
|
221
230
|
* @param {Column} column
|
|
222
231
|
*/
|
|
223
232
|
const dropColumn = (tableBuilder, column) => {
|
|
224
|
-
|
|
233
|
+
if (!db.config.settings.forceMigration) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return tableBuilder.dropColumn(column.name);
|
|
225
238
|
};
|
|
226
239
|
|
|
227
240
|
/**
|
|
@@ -316,7 +329,13 @@ const createHelpers = db => {
|
|
|
316
329
|
* @param {Knex.SchemaBuilder} schemaBuilder
|
|
317
330
|
* @param {Table} table
|
|
318
331
|
*/
|
|
319
|
-
const dropTable = (schemaBuilder, table) =>
|
|
332
|
+
const dropTable = (schemaBuilder, table) => {
|
|
333
|
+
if (!db.config.settings.forceMigration) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return schemaBuilder.dropTableIfExists(table.name);
|
|
338
|
+
};
|
|
320
339
|
|
|
321
340
|
/**
|
|
322
341
|
* Creates a table foreign keys constraints
|
|
@@ -336,6 +355,10 @@ const createHelpers = db => {
|
|
|
336
355
|
* @param {Table} table
|
|
337
356
|
*/
|
|
338
357
|
const dropTableForeignKeys = async (schemaBuilder, table) => {
|
|
358
|
+
if (!db.config.settings.forceMigration) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
339
362
|
// foreign keys
|
|
340
363
|
await schemaBuilder.table(table.name, tableBuilder => {
|
|
341
364
|
(table.foreignKeys || []).forEach(foreignKey => dropForeignKey(tableBuilder, foreignKey));
|
package/lib/schema/storage.js
CHANGED
|
@@ -5,10 +5,10 @@ const crypto = require('crypto');
|
|
|
5
5
|
const TABLE_NAME = 'strapi_database_schema';
|
|
6
6
|
|
|
7
7
|
module.exports = db => {
|
|
8
|
-
const hasSchemaTable = () => db.
|
|
8
|
+
const hasSchemaTable = () => db.getSchemaConnection().hasTable(TABLE_NAME);
|
|
9
9
|
|
|
10
10
|
const createSchemaTable = () => {
|
|
11
|
-
return db.
|
|
11
|
+
return db.getSchemaConnection().createTable(TABLE_NAME, t => {
|
|
12
12
|
t.increments('id');
|
|
13
13
|
t.json('schema');
|
|
14
14
|
t.datetime('time', { useTz: false });
|
|
@@ -26,7 +26,8 @@ module.exports = db => {
|
|
|
26
26
|
async read() {
|
|
27
27
|
await checkTableExists();
|
|
28
28
|
|
|
29
|
-
const res = await db
|
|
29
|
+
const res = await db
|
|
30
|
+
.getConnection()
|
|
30
31
|
.select('*')
|
|
31
32
|
.from(TABLE_NAME)
|
|
32
33
|
.orderBy('time', 'DESC')
|
|
@@ -55,21 +56,24 @@ module.exports = db => {
|
|
|
55
56
|
await checkTableExists();
|
|
56
57
|
|
|
57
58
|
// NOTE: we can remove this to add history
|
|
58
|
-
await db.
|
|
59
|
+
await db.getConnection(TABLE_NAME).delete();
|
|
59
60
|
|
|
60
61
|
const time = new Date();
|
|
61
62
|
|
|
62
|
-
await db
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
await db
|
|
64
|
+
.getConnection()
|
|
65
|
+
.insert({
|
|
66
|
+
schema: JSON.stringify(schema),
|
|
67
|
+
hash: this.hashSchema(schema),
|
|
68
|
+
time,
|
|
69
|
+
})
|
|
70
|
+
.into(TABLE_NAME);
|
|
67
71
|
},
|
|
68
72
|
|
|
69
73
|
async clear() {
|
|
70
74
|
await checkTableExists();
|
|
71
75
|
|
|
72
|
-
await db.
|
|
76
|
+
await db.getConnection(TABLE_NAME).truncate();
|
|
73
77
|
},
|
|
74
78
|
};
|
|
75
79
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strapi/database",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.31",
|
|
4
4
|
"description": "Strapi's database layer",
|
|
5
5
|
"homepage": "https://strapi.io",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"lodash": "4.17.21",
|
|
36
36
|
"umzug": "2.3.0"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "965d27fa02fd6908db602a25069795f304167f2c"
|
|
39
39
|
}
|
package/examples/data.sqlite
DELETED
|
Binary file
|