@strapi/database 0.0.0-fd8e4c6bfa → 0.0.0-ffd0a215143bcafd0354d791533a57a084e53ffb

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 (44) hide show
  1. package/lib/__tests__/index.test.js +93 -0
  2. package/lib/dialects/dialect.js +8 -0
  3. package/lib/dialects/mysql/constants.js +6 -0
  4. package/lib/dialects/mysql/database-inspector.js +37 -0
  5. package/lib/dialects/mysql/index.js +19 -0
  6. package/lib/dialects/postgresql/index.js +18 -3
  7. package/lib/dialects/sqlite/index.js +10 -2
  8. package/lib/entity-manager/__tests__/relations-orderer.test.js +164 -0
  9. package/lib/entity-manager/__tests__/sort-connect-array.test.js +79 -0
  10. package/lib/{entity-repository.js → entity-manager/entity-repository.js} +51 -12
  11. package/lib/{entity-manager.js → entity-manager/index.js} +526 -132
  12. package/lib/entity-manager/morph-relations.js +63 -0
  13. package/lib/entity-manager/regular-relations.js +387 -0
  14. package/lib/entity-manager/relations-orderer.js +225 -0
  15. package/lib/errors/index.js +2 -0
  16. package/lib/errors/invalid-relation.js +14 -0
  17. package/lib/index.d.ts +16 -4
  18. package/lib/index.js +47 -5
  19. package/lib/lifecycles/index.js +1 -1
  20. package/lib/metadata/index.js +26 -2
  21. package/lib/metadata/relations.js +86 -5
  22. package/lib/migrations/index.js +1 -1
  23. package/lib/query/helpers/index.js +1 -0
  24. package/lib/query/helpers/join.js +10 -9
  25. package/lib/query/helpers/populate/apply.js +664 -0
  26. package/lib/query/helpers/populate/index.js +9 -0
  27. package/lib/query/helpers/populate/process.js +102 -0
  28. package/lib/query/helpers/streams/index.js +5 -0
  29. package/lib/query/helpers/streams/readable.js +174 -0
  30. package/lib/query/helpers/transform.js +1 -1
  31. package/lib/query/helpers/where.js +16 -5
  32. package/lib/query/query-builder.js +128 -23
  33. package/lib/schema/builder.js +12 -3
  34. package/lib/schema/index.d.ts +2 -2
  35. package/lib/schema/schema.js +1 -1
  36. package/lib/tests/knex-utils.test.api.js +33 -0
  37. package/lib/tests/transactions.test.api.js +308 -0
  38. package/lib/transaction-context.js +17 -0
  39. package/lib/utils/knex.js +22 -0
  40. package/lib/validations/index.js +20 -0
  41. package/lib/validations/relations/bidirectional.js +89 -0
  42. package/lib/validations/relations/index.js +14 -0
  43. package/package.json +7 -6
  44. package/lib/query/helpers/populate.js +0 -649
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ const { Database } = require('../index');
4
+
5
+ jest.mock('../connection', () =>
6
+ jest.fn(() => {
7
+ const trx = {
8
+ commit: jest.fn(),
9
+ rollback: jest.fn(),
10
+ };
11
+ return {
12
+ ...trx,
13
+ transaction: jest.fn(async () => trx),
14
+ };
15
+ })
16
+ );
17
+
18
+ jest.mock('../dialects', () => ({
19
+ getDialect: jest.fn(() => ({
20
+ configure: jest.fn(),
21
+ initialize: jest.fn(),
22
+ })),
23
+ }));
24
+
25
+ jest.mock('../migrations', () => ({
26
+ createMigrationsProvider: jest.fn(),
27
+ }));
28
+
29
+ const config = {
30
+ models: [
31
+ {
32
+ tableName: 'strapi_core_store_settings',
33
+ },
34
+ ],
35
+ connection: {
36
+ client: 'postgres',
37
+ connection: {
38
+ database: 'strapi',
39
+ user: 'strapi',
40
+ password: 'strapi',
41
+ port: 5432,
42
+ host: 'localhost',
43
+ },
44
+ },
45
+ };
46
+
47
+ describe('Database', () => {
48
+ describe('constructor', () => {
49
+ it('should throw an error if no config is provided', async () => {
50
+ expect(async () => Database.init()).rejects.toThrowError();
51
+ });
52
+
53
+ it('it should intialize if config is provided', async () => {
54
+ expect(() => Database.init(config)).toBeDefined();
55
+ });
56
+ });
57
+
58
+ describe('Transaction', () => {
59
+ it('transaction should be defined', async () => {
60
+ const db = await Database.init(config);
61
+ expect(db.transaction).toBeDefined();
62
+ });
63
+
64
+ it('should return value if transaction is complete', async () => {
65
+ const db = await Database.init(config);
66
+ const result = await db.transaction(async () => 'test');
67
+ expect(result).toBe('test');
68
+ expect(db.connection.commit).toHaveBeenCalledTimes(1);
69
+ });
70
+
71
+ it('rollback is called incase of error', async () => {
72
+ const db = await Database.init(config);
73
+ try {
74
+ await db.transaction(async () => {
75
+ throw new Error('test');
76
+ });
77
+ } catch {
78
+ /* ignore */
79
+ }
80
+ expect(db.connection.rollback).toHaveBeenCalledTimes(1);
81
+ });
82
+
83
+ it('should throw error', async () => {
84
+ const db = await Database.init(config);
85
+
86
+ expect(async () => {
87
+ await db.transaction(async () => {
88
+ throw new Error('test');
89
+ });
90
+ }).rejects.toThrowError('test');
91
+ });
92
+ });
93
+ });
@@ -29,6 +29,10 @@ class Dialect {
29
29
  return false;
30
30
  }
31
31
 
32
+ supportsWindowFunctions() {
33
+ return true;
34
+ }
35
+
32
36
  async startSchemaUpdate() {
33
37
  // noop
34
38
  }
@@ -44,6 +48,10 @@ class Dialect {
44
48
 
45
49
  throw new Error(error.message);
46
50
  }
51
+
52
+ canAddIncrements() {
53
+ return true;
54
+ }
47
55
  }
48
56
 
49
57
  module.exports = {
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ MYSQL: 'MYSQL',
5
+ MARIADB: 'MARIADB',
6
+ };
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const { MARIADB, MYSQL } = require('./constants');
4
+
5
+ const SQL_QUERIES = {
6
+ VERSION: `SELECT version() as version`,
7
+ };
8
+
9
+ class MysqlDatabaseInspector {
10
+ constructor(db) {
11
+ this.db = db;
12
+ }
13
+
14
+ async getInformation() {
15
+ let database;
16
+ let versionNumber;
17
+ try {
18
+ const [results] = await this.db.connection.raw(SQL_QUERIES.VERSION);
19
+ const versionSplit = results[0].version.split('-');
20
+ const databaseName = versionSplit[1];
21
+ versionNumber = versionSplit[0];
22
+ database = databaseName && databaseName.toLowerCase() === 'mariadb' ? MARIADB : MYSQL;
23
+ } catch (e) {
24
+ return {
25
+ database: null,
26
+ version: null,
27
+ };
28
+ }
29
+
30
+ return {
31
+ database,
32
+ version: versionNumber,
33
+ };
34
+ }
35
+ }
36
+
37
+ module.exports = MysqlDatabaseInspector;
@@ -1,13 +1,19 @@
1
1
  'use strict';
2
2
 
3
+ const semver = require('semver');
4
+
3
5
  const { Dialect } = require('../dialect');
4
6
  const MysqlSchemaInspector = require('./schema-inspector');
7
+ const MysqlDatabaseInspector = require('./database-inspector');
8
+ const { MYSQL } = require('./constants');
5
9
 
6
10
  class MysqlDialect extends Dialect {
7
11
  constructor(db) {
8
12
  super(db);
9
13
 
10
14
  this.schemaInspector = new MysqlSchemaInspector(db);
15
+ this.databaseInspector = new MysqlDatabaseInspector(db);
16
+ this.info = null;
11
17
  }
12
18
 
13
19
  configure() {
@@ -38,6 +44,8 @@ class MysqlDialect extends Dialect {
38
44
  } catch (err) {
39
45
  // Ignore error due to lack of session permissions
40
46
  }
47
+
48
+ this.info = await this.databaseInspector.getInformation();
41
49
  }
42
50
 
43
51
  async startSchemaUpdate() {
@@ -57,6 +65,17 @@ class MysqlDialect extends Dialect {
57
65
  return true;
58
66
  }
59
67
 
68
+ supportsWindowFunctions() {
69
+ const isMysqlDB = !this.info.database || this.info.database === MYSQL;
70
+ const isBeforeV8 = !semver.valid(this.info.version) || semver.lt(this.info.version, '8.0.0');
71
+
72
+ if (isMysqlDB && isBeforeV8) {
73
+ return false;
74
+ }
75
+
76
+ return true;
77
+ }
78
+
60
79
  usesForeignKeys() {
61
80
  return true;
62
81
  }
@@ -15,9 +15,24 @@ class PostgresDialect extends Dialect {
15
15
  return true;
16
16
  }
17
17
 
18
- initialize() {
19
- this.db.connection.client.driver.types.setTypeParser(1082, 'text', (v) => v); // Don't cast DATE string to Date()
20
- this.db.connection.client.driver.types.setTypeParser(1700, 'text', parseFloat);
18
+ async initialize() {
19
+ // Don't cast DATE string to Date()
20
+ this.db.connection.client.driver.types.setTypeParser(
21
+ this.db.connection.client.driver.types.builtins.DATE,
22
+ 'text',
23
+ (v) => v
24
+ );
25
+ // Don't parse JSONB automatically
26
+ this.db.connection.client.driver.types.setTypeParser(
27
+ this.db.connection.client.driver.types.builtins.JSONB,
28
+ 'text',
29
+ (v) => v
30
+ );
31
+ this.db.connection.client.driver.types.setTypeParser(
32
+ this.db.connection.client.driver.types.builtins.NUMERIC,
33
+ 'text',
34
+ parseFloat
35
+ );
21
36
  }
22
37
 
23
38
  usesForeignKeys() {
@@ -5,13 +5,13 @@ const fse = require('fs-extra');
5
5
 
6
6
  const errors = require('../../errors');
7
7
  const { Dialect } = require('../dialect');
8
- const SqliteSchmeaInspector = require('./schema-inspector');
8
+ const SqliteSchemaInspector = require('./schema-inspector');
9
9
 
10
10
  class SqliteDialect extends Dialect {
11
11
  constructor(db) {
12
12
  super(db);
13
13
 
14
- this.schemaInspector = new SqliteSchmeaInspector(db);
14
+ this.schemaInspector = new SqliteSchemaInspector(db);
15
15
  }
16
16
 
17
17
  configure() {
@@ -24,6 +24,10 @@ class SqliteDialect extends Dialect {
24
24
  fse.ensureDirSync(dbDir);
25
25
  }
26
26
 
27
+ useReturning() {
28
+ return true;
29
+ }
30
+
27
31
  async initialize() {
28
32
  await this.db.connection.raw('pragma foreign_keys = on');
29
33
  }
@@ -68,6 +72,10 @@ class SqliteDialect extends Dialect {
68
72
  }
69
73
  }
70
74
  }
75
+
76
+ canAddIncrements() {
77
+ return false;
78
+ }
71
79
  }
72
80
 
73
81
  module.exports = SqliteDialect;
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ const { relationsOrderer } = require('../relations-orderer');
4
+
5
+ describe('Given I have some relations in the database', () => {
6
+ describe('When I connect a relation at the end', () => {
7
+ test('Then it is placed at the end with the correct order', () => {
8
+ const orderer = relationsOrderer(
9
+ [
10
+ { id: 2, order: 4 },
11
+ { id: 3, order: 10 },
12
+ ],
13
+ 'id',
14
+ 'order'
15
+ );
16
+
17
+ orderer.connect([{ id: 4, position: { end: true } }, { id: 5 }]);
18
+
19
+ expect(orderer.get()).toMatchObject([
20
+ { id: 2, order: 4 },
21
+ { id: 3, order: 10 },
22
+ { id: 4, order: 10.5 },
23
+ { id: 5, order: 10.5 },
24
+ ]);
25
+ });
26
+ });
27
+
28
+ describe('When I connect a relation at the start', () => {
29
+ test('Then it is placed at the start with the correct order', () => {
30
+ const orderer = relationsOrderer(
31
+ [
32
+ { id: 2, order: 4 },
33
+ { id: 3, order: 10 },
34
+ ],
35
+ 'id',
36
+ 'order'
37
+ );
38
+
39
+ orderer.connect([{ id: 4, position: { start: true } }]);
40
+
41
+ expect(orderer.get()).toMatchObject([
42
+ { id: 4, order: 0.5 },
43
+ { id: 2, order: 4 },
44
+ { id: 3, order: 10 },
45
+ ]);
46
+ });
47
+ });
48
+
49
+ describe('When I connect multiple relations using before', () => {
50
+ test('Then they are correctly ordered', () => {
51
+ const orderer = relationsOrderer(
52
+ [
53
+ { id: 2, order: 4 },
54
+ { id: 3, order: 10 },
55
+ ],
56
+ 'id',
57
+ 'order'
58
+ );
59
+
60
+ orderer.connect([
61
+ { id: 4, position: { before: 3 } },
62
+ { id: 5, position: { before: 4 } },
63
+ ]);
64
+
65
+ expect(orderer.get()).toMatchObject([
66
+ { id: 2, order: 4 },
67
+ { id: 5, order: 9.5 },
68
+ { id: 4, order: 9.5 },
69
+ { id: 3, order: 10 },
70
+ ]);
71
+ });
72
+ });
73
+
74
+ describe('When you connect multiple disordered relations', () => {
75
+ test('Then they are correctly ordered', () => {
76
+ const orderer = relationsOrderer(
77
+ [
78
+ { id: 1, order: 1 },
79
+ { id: 2, order: 2 },
80
+ { id: 3, order: 3 },
81
+ ],
82
+ 'id',
83
+ 'order'
84
+ );
85
+
86
+ orderer.connect([
87
+ { id: 5, position: { before: 1 } },
88
+ { id: 1, position: { before: 2 } },
89
+ { id: 2, position: { end: true } },
90
+ ]);
91
+
92
+ expect(orderer.get()).toMatchObject([
93
+ { id: 5, order: 0.5 },
94
+ { id: 1, order: 1.5 },
95
+ { id: 3, order: 3 },
96
+ { id: 2, order: 3.5 },
97
+ ]);
98
+ });
99
+ });
100
+
101
+ describe('When you connect a relation before a non-existing relation in non-strict mode', () => {
102
+ test('Then it is placed at the end', () => {
103
+ const orderer = relationsOrderer(
104
+ [
105
+ { id: 1, order: 1 },
106
+ { id: 2, order: 2 },
107
+ { id: 3, order: 3 },
108
+ ],
109
+ 'id',
110
+ 'order',
111
+ false
112
+ );
113
+
114
+ orderer.connect([{ id: 4, position: { before: 5 } }]);
115
+
116
+ expect(orderer.get()).toMatchObject([
117
+ { id: 1, order: 1 },
118
+ { id: 2, order: 2 },
119
+ { id: 3, order: 3 },
120
+ { id: 4, order: 3.5 },
121
+ ]);
122
+ });
123
+ });
124
+ });
125
+
126
+ describe('Given there are no relations in the database', () => {
127
+ describe('When you connect multiple new relations', () => {
128
+ test('Then they are correctly ordered', () => {
129
+ const orderer = relationsOrderer([], 'id', 'order');
130
+
131
+ orderer.connect([
132
+ { id: 1, position: { start: true } },
133
+ { id: 2, position: { start: true } },
134
+ { id: 3, position: { after: 1 } },
135
+ ]);
136
+
137
+ expect(orderer.get()).toMatchObject([
138
+ { id: 2, order: 0.5 },
139
+ { id: 1, order: 0.5 },
140
+ { id: 3, order: 0.5 },
141
+ ]);
142
+ });
143
+ });
144
+
145
+ describe('When you connect multiple disordered relations', () => {
146
+ test('Then they are correctly ordered', () => {
147
+ const orderer = relationsOrderer([], 'id', 'order');
148
+
149
+ orderer.connect([
150
+ { id: 5, position: { before: 1 } },
151
+ { id: 1, position: { before: 2 } },
152
+ { id: 2, position: { end: true } },
153
+ { id: 3, position: { after: 1 } },
154
+ ]);
155
+
156
+ expect(orderer.get()).toMatchObject([
157
+ { id: 5, order: 0.5 },
158
+ { id: 1, order: 0.5 },
159
+ { id: 3, order: 0.5 },
160
+ { id: 2, order: 0.5 },
161
+ ]);
162
+ });
163
+ });
164
+ });
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ const { sortConnectArray } = require('../relations-orderer');
4
+
5
+ describe('sortConnectArray', () => {
6
+ test('sorts connect array', () => {
7
+ const sortConnect = sortConnectArray([
8
+ { id: 5, position: { before: 1 } },
9
+ { id: 1, position: { before: 2 } },
10
+ { id: 2, position: { end: true } },
11
+ { id: 3, position: { after: 1 } },
12
+ ]);
13
+
14
+ expect(sortConnect).toMatchObject([
15
+ { id: 2, position: { end: true } },
16
+ { id: 1, position: { before: 2 } },
17
+ { id: 5, position: { before: 1 } },
18
+ { id: 3, position: { after: 1 } },
19
+ ]);
20
+ });
21
+
22
+ test('sorts connect array with initial relations', () => {
23
+ const sortConnect = sortConnectArray(
24
+ [
25
+ { id: 5, position: { before: 1 } },
26
+ { id: 1, position: { before: 2 } },
27
+ { id: 2, position: { end: true } },
28
+ { id: 3, position: { after: 1 } },
29
+ ],
30
+ [{ id: 1 }]
31
+ );
32
+
33
+ expect(sortConnect).toMatchObject([
34
+ { id: 5, position: { before: 1 } },
35
+ { id: 2, position: { end: true } },
36
+ { id: 1, position: { before: 2 } },
37
+ { id: 3, position: { after: 1 } },
38
+ ]);
39
+ });
40
+
41
+ test("error if position doesn't exist", () => {
42
+ const sortConnect = () => sortConnectArray([{ id: 1, position: { after: 2 } }]);
43
+
44
+ expect(sortConnect).toThrowError(
45
+ 'There was a problem connecting relation with id 1 at position {"after":2}. The relation with id 2 needs to be connected first.'
46
+ );
47
+ });
48
+
49
+ test('error with circular references', () => {
50
+ const sortConnect = () =>
51
+ sortConnectArray(
52
+ [
53
+ { id: 2, position: { after: 1 } },
54
+ { id: 3, position: { after: 1 } },
55
+ { id: 1, position: { after: 3 } },
56
+ ],
57
+ []
58
+ );
59
+
60
+ expect(sortConnect).toThrowError(
61
+ 'A circular reference was found in the connect array. One relation is trying to connect before/after another one that is trying to connect before/after it'
62
+ );
63
+ });
64
+
65
+ test('error when connecting same relation twice', () => {
66
+ const sortConnect = () =>
67
+ sortConnectArray(
68
+ [
69
+ { id: 1, position: { after: 2 } },
70
+ { id: 1, position: { after: 3 } },
71
+ ],
72
+ []
73
+ );
74
+
75
+ expect(sortConnect).toThrowError(
76
+ 'The relation with id 1 is already connected. You cannot connect the same relation twice.'
77
+ );
78
+ });
79
+ });
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ const { isString } = require('lodash/fp');
4
+ const { isAnyToMany } = require('../metadata/relations');
5
+
3
6
  const withDefaultPagination = (params) => {
4
7
  const { page = 1, pageSize = 10, ...rest } = params;
5
8
 
@@ -10,6 +13,21 @@ const withDefaultPagination = (params) => {
10
13
  };
11
14
  };
12
15
 
16
+ const withOffsetLimit = (params) => {
17
+ const { page, pageSize, ...rest } = withDefaultPagination(params);
18
+
19
+ const offset = Math.max(page - 1, 0) * pageSize;
20
+ const limit = pageSize;
21
+
22
+ const query = {
23
+ ...rest,
24
+ limit,
25
+ offset,
26
+ };
27
+
28
+ return [query, { page, pageSize }];
29
+ };
30
+
13
31
  const createRepository = (uid, db) => {
14
32
  return {
15
33
  findOne(params) {
@@ -28,16 +46,7 @@ const createRepository = (uid, db) => {
28
46
  },
29
47
 
30
48
  async findPage(params) {
31
- const { page, pageSize, ...rest } = withDefaultPagination(params);
32
-
33
- const offset = Math.max(page - 1, 0) * pageSize;
34
- const limit = pageSize;
35
-
36
- const query = {
37
- ...rest,
38
- limit,
39
- offset,
40
- };
49
+ const [query, { page, pageSize }] = withOffsetLimit(params);
41
50
 
42
51
  const [results, total] = await Promise.all([
43
52
  db.entityManager.findMany(uid, query),
@@ -99,8 +108,38 @@ const createRepository = (uid, db) => {
99
108
  return db.entityManager.populate(uid, entity, populate);
100
109
  },
101
110
 
102
- load(entity, field, params) {
103
- return db.entityManager.load(uid, entity, field, params);
111
+ load(entity, fields, params) {
112
+ return db.entityManager.load(uid, entity, fields, params);
113
+ },
114
+
115
+ async loadPages(entity, field, params) {
116
+ if (!isString(field)) {
117
+ throw new Error(`Invalid load. Expected ${field} to be a string`);
118
+ }
119
+
120
+ const { attributes } = db.metadata.get(uid);
121
+ const attribute = attributes[field];
122
+
123
+ if (!attribute || attribute.type !== 'relation' || !isAnyToMany(attribute)) {
124
+ throw new Error(`Invalid load. Expected ${field} to be an anyToMany relational attribute`);
125
+ }
126
+
127
+ const [query, { page, pageSize }] = withOffsetLimit(params);
128
+
129
+ const [results, { count: total }] = await Promise.all([
130
+ db.entityManager.load(uid, entity, field, query),
131
+ db.entityManager.load(uid, entity, field, { ...query, count: true }),
132
+ ]);
133
+
134
+ return {
135
+ results,
136
+ pagination: {
137
+ page,
138
+ pageSize,
139
+ pageCount: Math.ceil(total / pageSize),
140
+ total,
141
+ },
142
+ };
104
143
  },
105
144
  };
106
145
  };