@akemona-org/strapi-database 3.7.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/README.md +40 -0
- package/lib/connector-registry.js +63 -0
- package/lib/constants/index.js +53 -0
- package/lib/database-manager.js +178 -0
- package/lib/index.js +7 -0
- package/lib/lifecycle-manager.js +33 -0
- package/lib/migration-manager.js +66 -0
- package/lib/queries/create-query.js +107 -0
- package/lib/queries/helpers.js +35 -0
- package/lib/queries/index.js +7 -0
- package/lib/queries/paginated-queries.js +53 -0
- package/lib/queries/relations-counts-queries.js +53 -0
- package/lib/require-connector.js +37 -0
- package/lib/utils/lifecycles.js +24 -0
- package/lib/utils/primary-key.js +19 -0
- package/lib/validation/check-duplicated-table-names.js +60 -0
- package/lib/validation/check-reserved-names.js +64 -0
- package/lib/validation/index.js +13 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2015-present Strapi Solutions SAS
|
|
2
|
+
|
|
3
|
+
Portions of the Strapi software are licensed as follows:
|
|
4
|
+
|
|
5
|
+
* All software that resides under an "ee/" directory (the “EE Software”), if that directory exists, is licensed under the license defined in "ee/LICENSE".
|
|
6
|
+
|
|
7
|
+
* All software outside of the above-mentioned directories or restrictions above is available under the "MIT Expat" license as set forth below.
|
|
8
|
+
|
|
9
|
+
MIT Expat License
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Strapi database layer
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.org/package/strapi-database)
|
|
4
|
+
[](https://www.npmjs.org/package/strapi-database)
|
|
5
|
+
[](https://david-dm.org/strapi/strapi-database)
|
|
6
|
+
[](https://travis-ci.org/strapi/strapi-database)
|
|
7
|
+
[](https://slack.strapi.io)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Deprecation Warning :warning:
|
|
12
|
+
|
|
13
|
+
Hello! We have some news to share,
|
|
14
|
+
|
|
15
|
+
We’ve decided it’ll soon be time to end the support for `strapi-database`.
|
|
16
|
+
|
|
17
|
+
After years of iterations, Strapi is going to V4 and we won’t maintain V3 packages when it’ll reach its end-of-support milestone (~end of Q3 2022).
|
|
18
|
+
|
|
19
|
+
If you’ve been using `strapi-database` and have migrated to V4 (or if you want to), you can find the equivalent and updated version of this package at this [URL](https://github.com/strapi/strapi/tree/master/packages/core/database) and with the following name on NPM: `@strapi/database`.
|
|
20
|
+
|
|
21
|
+
If you’ve contributed to the development of this package, thank you again for that! We hope to see you on the V4 soon.
|
|
22
|
+
|
|
23
|
+
The Akemona team
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
This package is strapi's database handling layer. It is responsible for orchestrating database connectors and implementing the logic of strapi's data structures.
|
|
28
|
+
|
|
29
|
+
This package is not meant to be used as a standalone module.
|
|
30
|
+
|
|
31
|
+
## Resources
|
|
32
|
+
|
|
33
|
+
- [License](LICENSE)
|
|
34
|
+
|
|
35
|
+
## Links
|
|
36
|
+
|
|
37
|
+
- [Strapi documentation](https://strapi.akemona.com/documentation)
|
|
38
|
+
- [Strapi website](https://strapi.akemona.com/)
|
|
39
|
+
- [Strapi community on Slack](https://slack.strapi.io)
|
|
40
|
+
- [Strapi news on Twitter](https://twitter.com/strapijs)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Database connector registry
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const _ = require('lodash');
|
|
7
|
+
const requireConnector = require('./require-connector');
|
|
8
|
+
|
|
9
|
+
const createConnectorRegistry = ({ defaultConnection, connections }) => {
|
|
10
|
+
const _connectors = new Map();
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
/**
|
|
14
|
+
* Load connector modules
|
|
15
|
+
*/
|
|
16
|
+
load() {
|
|
17
|
+
for (const connection of Object.values(connections)) {
|
|
18
|
+
const { connector } = connection;
|
|
19
|
+
if (!_connectors.has(connector)) {
|
|
20
|
+
_connectors.set(connector, requireConnector(connector)(strapi));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initialize connectors
|
|
27
|
+
*/
|
|
28
|
+
async initialize() {
|
|
29
|
+
for (const connector of _connectors.values()) {
|
|
30
|
+
await connector.initialize();
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
getAll() {
|
|
35
|
+
return Array.from(_connectors.values());
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
get(key) {
|
|
39
|
+
return _connectors.get(key);
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
set(key, val) {
|
|
43
|
+
_connectors.set(key, val);
|
|
44
|
+
return this;
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
get default() {
|
|
48
|
+
const defaultConnector = connections[defaultConnection].connector;
|
|
49
|
+
return _connectors.get(defaultConnector);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
getByConnection(connection) {
|
|
53
|
+
if (!_.has(connections, connection)) {
|
|
54
|
+
throw new Error('Trying to access a connector for an unknown connection');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const connectorKey = connections[connection].connector;
|
|
58
|
+
return _connectors.get(connectorKey);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
module.exports = createConnectorRegistry;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { contentTypes: contentTypesUtils } = require('@akemona-org/strapi-utils');
|
|
4
|
+
|
|
5
|
+
const { PUBLISHED_AT_ATTRIBUTE, CREATED_BY_ATTRIBUTE, UPDATED_BY_ATTRIBUTE } =
|
|
6
|
+
contentTypesUtils.constants;
|
|
7
|
+
|
|
8
|
+
// contentTypes and components reserved names
|
|
9
|
+
const RESERVED_MODEL_NAMES = ['admin', 'boolean', 'date', 'date-time', 'time', 'upload'];
|
|
10
|
+
// attribute reserved names
|
|
11
|
+
const RESERVED_ATTRIBUTE_NAMES = [
|
|
12
|
+
// existing fields
|
|
13
|
+
'_id',
|
|
14
|
+
'id',
|
|
15
|
+
CREATED_BY_ATTRIBUTE,
|
|
16
|
+
UPDATED_BY_ATTRIBUTE,
|
|
17
|
+
PUBLISHED_AT_ATTRIBUTE,
|
|
18
|
+
|
|
19
|
+
// existing object properties that may cause trouble
|
|
20
|
+
'length',
|
|
21
|
+
'attributes',
|
|
22
|
+
'relations',
|
|
23
|
+
'changed',
|
|
24
|
+
|
|
25
|
+
// list found here https://mongoosejs.com/docs/api.html#schema_Schema.reserved
|
|
26
|
+
'_posts',
|
|
27
|
+
'_pres',
|
|
28
|
+
'collection',
|
|
29
|
+
'emit',
|
|
30
|
+
'errors',
|
|
31
|
+
'get',
|
|
32
|
+
'init',
|
|
33
|
+
'isModified',
|
|
34
|
+
'isNew',
|
|
35
|
+
'listeners',
|
|
36
|
+
'modelName',
|
|
37
|
+
'on',
|
|
38
|
+
'once',
|
|
39
|
+
'populated',
|
|
40
|
+
'prototype',
|
|
41
|
+
'remove',
|
|
42
|
+
'removeListener',
|
|
43
|
+
'save',
|
|
44
|
+
'schema',
|
|
45
|
+
'toObject',
|
|
46
|
+
'validate',
|
|
47
|
+
'format',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
RESERVED_MODEL_NAMES,
|
|
52
|
+
RESERVED_ATTRIBUTE_NAMES,
|
|
53
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
|
|
5
|
+
const { createQuery } = require('./queries');
|
|
6
|
+
const createConnectorRegistry = require('./connector-registry');
|
|
7
|
+
const constants = require('./constants');
|
|
8
|
+
const { validateModelSchemas } = require('./validation');
|
|
9
|
+
const createMigrationManager = require('./migration-manager');
|
|
10
|
+
const createLifecycleManager = require('./lifecycle-manager');
|
|
11
|
+
|
|
12
|
+
class DatabaseManager {
|
|
13
|
+
constructor(strapi) {
|
|
14
|
+
this.strapi = strapi;
|
|
15
|
+
|
|
16
|
+
this.initialized = false;
|
|
17
|
+
|
|
18
|
+
this.connectors = createConnectorRegistry({
|
|
19
|
+
connections: strapi.config.get('database.connections'),
|
|
20
|
+
defaultConnection: strapi.config.get('database.defaultConnection'),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
this.queries = new Map();
|
|
24
|
+
this.models = new Map();
|
|
25
|
+
|
|
26
|
+
this.migrations = createMigrationManager(this);
|
|
27
|
+
this.lifecycles = createLifecycleManager();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async initialize() {
|
|
31
|
+
if (this.initialized === true) {
|
|
32
|
+
throw new Error('Database manager already initialized');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.initialized = true;
|
|
36
|
+
|
|
37
|
+
this.connectors.load();
|
|
38
|
+
|
|
39
|
+
validateModelSchemas({ strapi: this.strapi, manager: this });
|
|
40
|
+
|
|
41
|
+
this.initializeModelsMap();
|
|
42
|
+
|
|
43
|
+
await this.connectors.initialize();
|
|
44
|
+
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async destroy() {
|
|
49
|
+
await Promise.all(this.connectors.getAll().map(connector => connector.destroy()));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
initializeModelsMap() {
|
|
53
|
+
Object.keys(this.strapi.models).forEach(modelKey => {
|
|
54
|
+
const model = this.strapi.models[modelKey];
|
|
55
|
+
this.models.set(model.uid, model);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
Object.keys(this.strapi.admin.models).forEach(modelKey => {
|
|
59
|
+
const model = this.strapi.admin.models[modelKey];
|
|
60
|
+
this.models.set(model.uid, model);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
Object.keys(this.strapi.plugins).forEach(pluginKey => {
|
|
64
|
+
Object.keys(this.strapi.plugins[pluginKey].models).forEach(modelKey => {
|
|
65
|
+
const model = this.strapi.plugins[pluginKey].models[modelKey];
|
|
66
|
+
this.models.set(model.uid, model);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
query(entity, plugin) {
|
|
72
|
+
if (!entity) {
|
|
73
|
+
throw new Error(`argument entity is required`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const model = this.getModel(entity, plugin);
|
|
77
|
+
|
|
78
|
+
if (!model) {
|
|
79
|
+
throw new Error(`The model ${entity} can't be found.`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (this.queries.has(model.uid)) {
|
|
83
|
+
return this.queries.get(model.uid);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const connectorQuery = this.connectors
|
|
87
|
+
.get(model.orm)
|
|
88
|
+
.queries({ model, modelKey: model.modelName, strapi });
|
|
89
|
+
|
|
90
|
+
const query = createQuery({
|
|
91
|
+
connectorQuery,
|
|
92
|
+
model,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
this.queries.set(model.uid, query);
|
|
96
|
+
return query;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getModelFromStrapi(name, plugin) {
|
|
100
|
+
const key = _.toLower(name);
|
|
101
|
+
if (plugin === 'admin') {
|
|
102
|
+
return _.get(strapi.admin, ['models', key]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (plugin) {
|
|
106
|
+
return _.get(strapi.plugins, [plugin, 'models', key]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return _.get(strapi, ['models', key]) || _.get(strapi, ['components', key]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getModel(name, plugin) {
|
|
113
|
+
const key = _.toLower(name);
|
|
114
|
+
|
|
115
|
+
if (this.models.has(key)) {
|
|
116
|
+
const { modelName, plugin: pluginName } = this.models.get(key);
|
|
117
|
+
return this.getModelFromStrapi(modelName, pluginName);
|
|
118
|
+
} else {
|
|
119
|
+
return this.getModelFromStrapi(key, plugin);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getModelByAssoc(assoc) {
|
|
124
|
+
return this.getModel(assoc.collection || assoc.model, assoc.plugin);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getModelByCollectionName(collectionName) {
|
|
128
|
+
return Array.from(this.models.values()).find(model => {
|
|
129
|
+
return model.collectionName === collectionName;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getModelByGlobalId(globalId) {
|
|
134
|
+
return Array.from(this.models.values()).find(model => {
|
|
135
|
+
return model.globalId === globalId;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
getModelsByAttribute(attr) {
|
|
140
|
+
if (attr.type === 'component') {
|
|
141
|
+
return [this.getModel(attr.component)];
|
|
142
|
+
}
|
|
143
|
+
if (attr.type === 'dynamiczone') {
|
|
144
|
+
return attr.components.map(compoName => this.getModel(compoName));
|
|
145
|
+
}
|
|
146
|
+
if (attr.model || attr.collection) {
|
|
147
|
+
return [this.getModelByAssoc(attr)];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
getModelsByPluginName(pluginName) {
|
|
154
|
+
if (!pluginName) {
|
|
155
|
+
return strapi.models;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return pluginName === 'admin' ? strapi.admin.models : strapi.plugins[pluginName].models;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getReservedNames() {
|
|
162
|
+
return {
|
|
163
|
+
models: constants.RESERVED_MODEL_NAMES,
|
|
164
|
+
attributes: [
|
|
165
|
+
...constants.RESERVED_ATTRIBUTE_NAMES,
|
|
166
|
+
...(strapi.db.connectors.default.defaultTimestamps || []),
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function createDatabaseManager(strapi) {
|
|
173
|
+
return new DatabaseManager(strapi);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
createDatabaseManager,
|
|
178
|
+
};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const debug = require('debug')('strapi-database:lifecycle');
|
|
3
|
+
const { isFunction, isNil } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
class LifecycleManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
debug('Initialize lifecycle manager');
|
|
8
|
+
this.lifecycles = [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
register(lifecycle) {
|
|
12
|
+
debug('Register lifecycle');
|
|
13
|
+
|
|
14
|
+
this.lifecycles.push(lifecycle);
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async run(action, model, ...args) {
|
|
19
|
+
for (const lifecycle of this.lifecycles) {
|
|
20
|
+
if (!isNil(lifecycle.model) && lifecycle.model !== model.uid) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (isFunction(lifecycle[action])) {
|
|
25
|
+
debug(`Run lifecycle ${action} for model ${model.uid}`);
|
|
26
|
+
|
|
27
|
+
await lifecycle[action](...args);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = () => new LifecycleManager();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const debug = require('debug')('strapi-database:migration');
|
|
3
|
+
const { isFunction, get } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
class MigrationManager {
|
|
6
|
+
constructor(db) {
|
|
7
|
+
debug('Initialize migration manager');
|
|
8
|
+
this.db = db;
|
|
9
|
+
this.migrations = [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
register(migration) {
|
|
13
|
+
debug('Register migration');
|
|
14
|
+
this.migrations.push(migration);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async run(fn, options, context = {}) {
|
|
18
|
+
debug('Run migration');
|
|
19
|
+
await this.runBefore(options, context);
|
|
20
|
+
await fn(options, context);
|
|
21
|
+
await this.runAfter(options, context);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async shouldRun({ migration, step, options, context }) {
|
|
25
|
+
const method = migration[step];
|
|
26
|
+
const shouldRunMethod = get(`shouldRun.${step}`, migration, null);
|
|
27
|
+
|
|
28
|
+
if (!isFunction(method)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!isFunction(shouldRunMethod)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return shouldRunMethod(options, context);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async runBefore(options, context) {
|
|
40
|
+
debug('Run before migrations');
|
|
41
|
+
|
|
42
|
+
for (const migration of this.migrations) {
|
|
43
|
+
const willRunStep = await this.shouldRun({ migration, step: 'before', options, context });
|
|
44
|
+
|
|
45
|
+
if (willRunStep) {
|
|
46
|
+
await migration.before(options, context);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async runAfter(options, context) {
|
|
52
|
+
debug('Run after migrations');
|
|
53
|
+
|
|
54
|
+
for (const migration of this.migrations.slice(0).reverse()) {
|
|
55
|
+
const willRunStep = await this.shouldRun({ migration, step: 'after', options, context });
|
|
56
|
+
|
|
57
|
+
if (willRunStep) {
|
|
58
|
+
await migration.after(options, context);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = strapi => {
|
|
65
|
+
return new MigrationManager(strapi);
|
|
66
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const pmap = require('p-map');
|
|
4
|
+
|
|
5
|
+
const { createQueryWithLifecycles, withLifecycles } = require('./helpers');
|
|
6
|
+
const { createRelationsCountsQuery } = require('./relations-counts-queries');
|
|
7
|
+
const { createFindPageQuery, createSearchPageQuery } = require('./paginated-queries');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {Object} opts options
|
|
11
|
+
* @param {Object} opts.model The ORM model
|
|
12
|
+
* @param {Object} opts.connectorQuery The ORM queries implementation
|
|
13
|
+
*/
|
|
14
|
+
module.exports = function createQuery(opts) {
|
|
15
|
+
const { model, connectorQuery } = opts;
|
|
16
|
+
|
|
17
|
+
const createFn = createQueryWithLifecycles({
|
|
18
|
+
query: 'create',
|
|
19
|
+
model,
|
|
20
|
+
connectorQuery,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const createMany = (entities, { concurrency = 100 } = {}, ...rest) => {
|
|
24
|
+
return pmap(entities, entity => createFn(entity, ...rest), {
|
|
25
|
+
concurrency,
|
|
26
|
+
stopOnError: true,
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const findPage = withLifecycles({
|
|
31
|
+
query: 'findPage',
|
|
32
|
+
model,
|
|
33
|
+
fn: createFindPageQuery(connectorQuery),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const findWithRelationCounts = createRelationsCountsQuery({
|
|
37
|
+
model,
|
|
38
|
+
fn: findPage,
|
|
39
|
+
connectorQuery,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const searchPage = withLifecycles({
|
|
43
|
+
query: 'searchPage',
|
|
44
|
+
model,
|
|
45
|
+
fn: createSearchPageQuery(connectorQuery),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const searchWithRelationCounts = createRelationsCountsQuery({
|
|
49
|
+
model,
|
|
50
|
+
fn: searchPage,
|
|
51
|
+
connectorQuery,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
get model() {
|
|
56
|
+
return model;
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
get orm() {
|
|
60
|
+
return model.orm;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
get primaryKey() {
|
|
64
|
+
return model.primaryKey;
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
get associations() {
|
|
68
|
+
return model.associations;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Run custom database logic
|
|
73
|
+
*/
|
|
74
|
+
custom(mapping) {
|
|
75
|
+
if (typeof mapping === 'function') {
|
|
76
|
+
return mapping.bind(this, { model: this.model });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!mapping[this.orm]) {
|
|
80
|
+
throw new Error(`Missing mapping for orm ${this.orm}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof mapping[this.orm] !== 'function') {
|
|
84
|
+
throw new Error(`Custom queries must be functions received ${typeof mapping[this.orm]}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return mapping[this.model.orm].call(this, { model: this.model });
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
create: createFn,
|
|
91
|
+
createMany,
|
|
92
|
+
update: createQueryWithLifecycles({ query: 'update', model, connectorQuery }),
|
|
93
|
+
delete: createQueryWithLifecycles({ query: 'delete', model, connectorQuery }),
|
|
94
|
+
find: createQueryWithLifecycles({ query: 'find', model, connectorQuery }),
|
|
95
|
+
findOne: createQueryWithLifecycles({ query: 'findOne', model, connectorQuery }),
|
|
96
|
+
count: createQueryWithLifecycles({ query: 'count', model, connectorQuery }),
|
|
97
|
+
search: createQueryWithLifecycles({ query: 'search', model, connectorQuery }),
|
|
98
|
+
countSearch: createQueryWithLifecycles({ query: 'countSearch', model, connectorQuery }),
|
|
99
|
+
|
|
100
|
+
// paginated queries
|
|
101
|
+
findPage,
|
|
102
|
+
searchPage,
|
|
103
|
+
|
|
104
|
+
searchWithRelationCounts,
|
|
105
|
+
findWithRelationCounts,
|
|
106
|
+
};
|
|
107
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { replaceIdByPrimaryKey } = require('../utils/primary-key');
|
|
4
|
+
const { executeBeforeLifecycle, executeAfterLifecycle } = require('../utils/lifecycles');
|
|
5
|
+
|
|
6
|
+
const withLifecycles = ({ query, model, fn }) => async (params, ...rest) => {
|
|
7
|
+
// substitute id for primaryKey value in params
|
|
8
|
+
const newParams = replaceIdByPrimaryKey(params, model);
|
|
9
|
+
const queryArguments = [newParams, ...rest];
|
|
10
|
+
|
|
11
|
+
// execute before hook
|
|
12
|
+
await executeBeforeLifecycle(query, model, ...queryArguments);
|
|
13
|
+
|
|
14
|
+
// execute query
|
|
15
|
+
const result = await fn(...queryArguments);
|
|
16
|
+
|
|
17
|
+
// execute after hook with result and arguments
|
|
18
|
+
await executeAfterLifecycle(query, model, result, ...queryArguments);
|
|
19
|
+
|
|
20
|
+
// return result
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// wraps a connectorQuery call with:
|
|
25
|
+
// - param substitution
|
|
26
|
+
// - lifecycle hooks
|
|
27
|
+
const createQueryWithLifecycles = ({ query, model, connectorQuery }) => {
|
|
28
|
+
return withLifecycles({
|
|
29
|
+
query,
|
|
30
|
+
model,
|
|
31
|
+
fn: (...queryParameters) => connectorQuery[query](...queryParameters),
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
module.exports = { withLifecycles, createQueryWithLifecycles };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
|
|
5
|
+
const createPaginatedQuery = ({ fetch, count }) => async (queryParams, ...args) => {
|
|
6
|
+
const params = _.omit(queryParams, ['page', 'pageSize']);
|
|
7
|
+
const pagination = await getPaginationInfos(queryParams, count, ...args);
|
|
8
|
+
|
|
9
|
+
Object.assign(params, paginationToQueryParams(pagination));
|
|
10
|
+
const results = await fetch(params, undefined, ...args);
|
|
11
|
+
|
|
12
|
+
return { results, pagination };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const createSearchPageQuery = ({ search, countSearch }) =>
|
|
16
|
+
createPaginatedQuery({ fetch: search, count: countSearch });
|
|
17
|
+
|
|
18
|
+
const createFindPageQuery = ({ find, count }) => createPaginatedQuery({ fetch: find, count });
|
|
19
|
+
|
|
20
|
+
const getPaginationInfos = async (queryParams, count, ...args) => {
|
|
21
|
+
const { page, pageSize, ...params } = withDefaultPagination(queryParams);
|
|
22
|
+
|
|
23
|
+
const total = await count(params, ...args);
|
|
24
|
+
return {
|
|
25
|
+
page,
|
|
26
|
+
pageSize,
|
|
27
|
+
pageCount: Math.ceil(total / pageSize),
|
|
28
|
+
total,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const withDefaultPagination = params => {
|
|
33
|
+
const { page = 1, pageSize = 100, ...rest } = params;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
page: parseInt(page),
|
|
37
|
+
pageSize: parseInt(pageSize),
|
|
38
|
+
...rest,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const paginationToQueryParams = ({ page, pageSize }) => ({
|
|
43
|
+
_start: Math.max(page - 1, 0) * pageSize,
|
|
44
|
+
_limit: pageSize,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
getPaginationInfos,
|
|
49
|
+
withDefaultPagination,
|
|
50
|
+
createPaginatedQuery,
|
|
51
|
+
createFindPageQuery,
|
|
52
|
+
createSearchPageQuery,
|
|
53
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { prop, assoc } = require('lodash/fp');
|
|
4
|
+
const { MANY_RELATIONS } = require('@akemona-org/strapi-utils').relations.constants;
|
|
5
|
+
const { isVisibleAttribute } = require('@akemona-org/strapi-utils').contentTypes;
|
|
6
|
+
|
|
7
|
+
const createRelationsCountsQuery = ({ model, fn, connectorQuery }) => {
|
|
8
|
+
// fetch counter map
|
|
9
|
+
const fetchCounters = async (...args) => {
|
|
10
|
+
const results = await connectorQuery.fetchRelationCounters(...args);
|
|
11
|
+
return results.reduce((map, { id, count }) => assoc(id, Number(count), map), {});
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return async function (params, populate) {
|
|
15
|
+
const toCount = [];
|
|
16
|
+
const toPopulate = [];
|
|
17
|
+
|
|
18
|
+
model.associations
|
|
19
|
+
.filter((assoc) => !populate || populate.includes(assoc.alias))
|
|
20
|
+
.forEach((assoc) => {
|
|
21
|
+
if (MANY_RELATIONS.includes(assoc.nature) && isVisibleAttribute(model, assoc.alias)) {
|
|
22
|
+
return toCount.push(assoc);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
toPopulate.push(assoc.alias);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const { results, pagination } = await fn(params, toPopulate);
|
|
29
|
+
const resultsIds = results.map(prop('id'));
|
|
30
|
+
|
|
31
|
+
const counters = await Promise.all(
|
|
32
|
+
toCount.map(async ({ alias }) => ({
|
|
33
|
+
field: alias,
|
|
34
|
+
counts: await fetchCounters(alias, resultsIds),
|
|
35
|
+
}))
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
results.forEach((entity) => {
|
|
39
|
+
counters.forEach(({ field, counts }) => {
|
|
40
|
+
entity[field] = { count: counts[entity.id] || 0 };
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
results,
|
|
46
|
+
pagination,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
createRelationsCountsQuery,
|
|
53
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const VError = require('verror');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Requires a database connector
|
|
7
|
+
* @param {string} connector connector name
|
|
8
|
+
* @param {DatabaseManager} databaseManager reference to the database manager
|
|
9
|
+
*/
|
|
10
|
+
module.exports = function requireConnector(connector) {
|
|
11
|
+
if (!connector) {
|
|
12
|
+
throw new VError(
|
|
13
|
+
{ name: 'ConnectorError' },
|
|
14
|
+
'initialize connector without name'
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
require.resolve(`strapi-connector-${connector}`);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
throw new VError(
|
|
22
|
+
{ name: 'ConnectorError', cause: error },
|
|
23
|
+
'connector "%s" not found',
|
|
24
|
+
connector
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
return require(`strapi-connector-${connector}`);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new VError(
|
|
32
|
+
{ name: 'ConnectorError', cause: error },
|
|
33
|
+
'initialize connector "%s"',
|
|
34
|
+
connector
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
|
|
5
|
+
const executeLifecycle = async (lifecycle, model, ...args) => {
|
|
6
|
+
// Run registered lifecycles
|
|
7
|
+
await strapi.db.lifecycles.run(lifecycle, model, ...args);
|
|
8
|
+
|
|
9
|
+
// Run user lifecycles
|
|
10
|
+
if (_.has(model, `lifecycles.${lifecycle}`)) {
|
|
11
|
+
await model.lifecycles[lifecycle](...args);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const executeBeforeLifecycle = (lifecycle, model, ...args) =>
|
|
16
|
+
executeLifecycle(`before${_.upperFirst(lifecycle)}`, model, ...args);
|
|
17
|
+
|
|
18
|
+
const executeAfterLifecycle = (lifecycle, model, ...args) =>
|
|
19
|
+
executeLifecycle(`after${_.upperFirst(lifecycle)}`, model, ...args);
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
executeBeforeLifecycle,
|
|
23
|
+
executeAfterLifecycle,
|
|
24
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* If exists, rename the key "id" by the primary key name of the model ("_id" by default for mongoose).
|
|
7
|
+
*/
|
|
8
|
+
const replaceIdByPrimaryKey = (params, model) => {
|
|
9
|
+
const newParams = { ...params };
|
|
10
|
+
if (_.has(params, 'id')) {
|
|
11
|
+
delete newParams.id;
|
|
12
|
+
newParams[model.primaryKey] = params[model.primaryKey] || params.id;
|
|
13
|
+
}
|
|
14
|
+
return newParams;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
replaceIdByPrimaryKey,
|
|
19
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
|
|
5
|
+
const createErrorMessage = (
|
|
6
|
+
modelA,
|
|
7
|
+
modelB
|
|
8
|
+
) => `Duplicated collection name: \`${modelA.model.collectionName}\`.
|
|
9
|
+
The same collection name can't be used for two different models.
|
|
10
|
+
First found in ${modelA.origin} \`${modelA.apiOrPluginName}\`, model \`${modelA.modelName}\`.
|
|
11
|
+
Second found in ${modelB.origin} \`${modelB.apiOrPluginName}\`, model \`${modelB.modelName}\`.`;
|
|
12
|
+
|
|
13
|
+
// Check if all collection names are unique
|
|
14
|
+
const checkDuplicatedTableNames = ({ strapi }) => {
|
|
15
|
+
const modelsWithInfo = [];
|
|
16
|
+
_.forIn(strapi.admin.models, (model, modelName) => {
|
|
17
|
+
modelsWithInfo.push({
|
|
18
|
+
origin: 'Strapi internal',
|
|
19
|
+
model,
|
|
20
|
+
apiOrPluginName: 'admin',
|
|
21
|
+
modelName,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
_.forIn(strapi.api, (api, apiName) => {
|
|
26
|
+
_.forIn(api.models, (model, modelName) => {
|
|
27
|
+
modelsWithInfo.push({
|
|
28
|
+
origin: 'API',
|
|
29
|
+
model,
|
|
30
|
+
apiOrPluginName: apiName,
|
|
31
|
+
modelName,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
_.forIn(strapi.plugins, (plugin, pluginName) => {
|
|
37
|
+
_.forIn(plugin.models, (model, modelName) => {
|
|
38
|
+
modelsWithInfo.push({
|
|
39
|
+
origin: 'Plugin',
|
|
40
|
+
model,
|
|
41
|
+
apiOrPluginName: pluginName,
|
|
42
|
+
modelName,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
modelsWithInfo.forEach(modelA => {
|
|
48
|
+
const similarModelFound = modelsWithInfo.find(
|
|
49
|
+
modelB =>
|
|
50
|
+
modelB.model.collectionName === modelA.model.collectionName &&
|
|
51
|
+
modelB.model.uid !== modelA.model.uid
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (similarModelFound) {
|
|
55
|
+
throw new Error(createErrorMessage(modelA, similarModelFound));
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
module.exports = checkDuplicatedTableNames;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const constants = require('../constants');
|
|
5
|
+
|
|
6
|
+
class ModelError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.stack = null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const checkReservedAttributeNames = (model, { manager }) => {
|
|
14
|
+
const reservedNames = [...constants.RESERVED_ATTRIBUTE_NAMES];
|
|
15
|
+
|
|
16
|
+
if (_.has(model, 'options.timestamps')) {
|
|
17
|
+
const [connectorCreatedAt, connectorUpdatedAt] = manager.connectors.getByConnection(
|
|
18
|
+
model.connection
|
|
19
|
+
).defaultTimestamps;
|
|
20
|
+
|
|
21
|
+
if (Array.isArray(model.options.timestamps)) {
|
|
22
|
+
const [
|
|
23
|
+
createdAtAttribute = connectorCreatedAt,
|
|
24
|
+
updatedAtAttribute = connectorUpdatedAt,
|
|
25
|
+
] = model.options.timestamps;
|
|
26
|
+
|
|
27
|
+
reservedNames.push(createdAtAttribute, updatedAtAttribute);
|
|
28
|
+
} else {
|
|
29
|
+
reservedNames.push(connectorCreatedAt, connectorUpdatedAt);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const usedReservedAttributeNames = _.intersection(Object.keys(model.attributes), reservedNames);
|
|
34
|
+
|
|
35
|
+
if (usedReservedAttributeNames.length > 0) {
|
|
36
|
+
throw new ModelError(
|
|
37
|
+
`Model "${
|
|
38
|
+
model.modelName
|
|
39
|
+
}" is using reserved attribute names "${usedReservedAttributeNames.join(
|
|
40
|
+
', '
|
|
41
|
+
)}".\n-> Make sure you are not using a reserved name or overriding the defined timestamp attributes.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const checkReservedModelName = model => {
|
|
47
|
+
if (constants.RESERVED_MODEL_NAMES.includes(model.modelName)) {
|
|
48
|
+
throw new ModelError(
|
|
49
|
+
`"${model.modelName}" is a reserved model name. You need to rename your model and the files associated with it.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const getModelsFrom = source => _.flatMap(source, iteratee => _.values(iteratee.models));
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Checks that there are no model using reserved names (content type, component, attributes)
|
|
58
|
+
*/
|
|
59
|
+
module.exports = ({ strapi, manager }) => {
|
|
60
|
+
[...getModelsFrom(strapi.api), ...getModelsFrom(strapi.plugins)].forEach(model => {
|
|
61
|
+
checkReservedModelName(model);
|
|
62
|
+
checkReservedAttributeNames(model, { manager });
|
|
63
|
+
});
|
|
64
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const checkDuplicatedTableNames = require('./check-duplicated-table-names');
|
|
4
|
+
const checkReservedNames = require('./check-reserved-names');
|
|
5
|
+
|
|
6
|
+
const validateModelSchemas = ({ strapi, manager }) => {
|
|
7
|
+
checkDuplicatedTableNames({ strapi, manager });
|
|
8
|
+
checkReservedNames({ strapi, manager });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
validateModelSchemas,
|
|
13
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@akemona-org/strapi-database",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "3.7.0",
|
|
7
|
+
"description": "Strapi's database layer",
|
|
8
|
+
"homepage": "https://strapi.akemona.com",
|
|
9
|
+
"main": "./lib/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"no tests yet\""
|
|
12
|
+
},
|
|
13
|
+
"directories": {
|
|
14
|
+
"lib": "./lib"
|
|
15
|
+
},
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "Akemona team",
|
|
18
|
+
"email": "strapi@akemona.com",
|
|
19
|
+
"url": "https://strapi.akemona.com"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git://github.com/akemona/strapi.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/akemona/strapi/issues"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=10.16.0 <=14.x.x",
|
|
30
|
+
"npm": ">=6.0.0"
|
|
31
|
+
},
|
|
32
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@akemona-org/strapi-utils": "3.7.0",
|
|
35
|
+
"debug": "4.3.1",
|
|
36
|
+
"lodash": "4.17.21",
|
|
37
|
+
"p-map": "4.0.0",
|
|
38
|
+
"verror": "^1.10.0"
|
|
39
|
+
},
|
|
40
|
+
"gitHead": "129a8d6191b55810fd66448dcc47fee829df986c"
|
|
41
|
+
}
|