@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 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
+ [![npm version](https://img.shields.io/npm/v/strapi-database.svg)](https://www.npmjs.org/package/strapi-database)
4
+ [![npm downloads](https://img.shields.io/npm/dm/strapi-database.svg)](https://www.npmjs.org/package/strapi-database)
5
+ [![npm dependencies](https://david-dm.org/strapi/strapi-database.svg)](https://david-dm.org/strapi/strapi-database)
6
+ [![Build status](https://travis-ci.org/strapi/strapi-database.svg?branch=master)](https://travis-ci.org/strapi/strapi-database)
7
+ [![Slack status](https://slack.strapi.io/badge.svg)](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,7 @@
1
+ 'use strict';
2
+
3
+ const { createDatabaseManager } = require('./database-manager');
4
+
5
+ module.exports = {
6
+ createDatabaseManager,
7
+ };
@@ -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,7 @@
1
+ 'use strict';
2
+
3
+ const createQuery = require('./create-query');
4
+
5
+ module.exports = {
6
+ createQuery,
7
+ };
@@ -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
+ }