@strapi/database 5.0.0-beta.1 → 5.0.0-beta.11

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 (56) hide show
  1. package/dist/entity-manager/index.d.ts.map +1 -1
  2. package/dist/entity-manager/morph-relations.d.ts +1 -1
  3. package/dist/entity-manager/morph-relations.d.ts.map +1 -1
  4. package/dist/entity-manager/regular-relations.d.ts +8 -8
  5. package/dist/entity-manager/regular-relations.d.ts.map +1 -1
  6. package/dist/entity-manager/relations-orderer.d.ts.map +1 -1
  7. package/dist/index.d.ts +7 -6
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +944 -342
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +942 -340
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/lifecycles/types.d.ts +1 -0
  14. package/dist/lifecycles/types.d.ts.map +1 -1
  15. package/dist/metadata/index.d.ts +1 -1
  16. package/dist/metadata/index.d.ts.map +1 -1
  17. package/dist/metadata/metadata.d.ts +2 -1
  18. package/dist/metadata/metadata.d.ts.map +1 -1
  19. package/dist/metadata/relations.d.ts.map +1 -1
  20. package/dist/migrations/common.d.ts +16 -2
  21. package/dist/migrations/common.d.ts.map +1 -1
  22. package/dist/migrations/index.d.ts +2 -2
  23. package/dist/migrations/index.d.ts.map +1 -1
  24. package/dist/migrations/internal-migrations/5.0.0-01-convert-identifiers-long-than-max-length.d.ts +3 -0
  25. package/dist/migrations/internal-migrations/5.0.0-01-convert-identifiers-long-than-max-length.d.ts.map +1 -0
  26. package/dist/migrations/internal-migrations/5.0.0-02-document-id.d.ts +3 -0
  27. package/dist/migrations/internal-migrations/5.0.0-02-document-id.d.ts.map +1 -0
  28. package/dist/migrations/internal-migrations/5.0.0-03-locale.d.ts +3 -0
  29. package/dist/migrations/internal-migrations/5.0.0-03-locale.d.ts.map +1 -0
  30. package/dist/migrations/internal-migrations/index.d.ts.map +1 -1
  31. package/dist/migrations/internal.d.ts +2 -2
  32. package/dist/migrations/internal.d.ts.map +1 -1
  33. package/dist/migrations/logger.d.ts +10 -0
  34. package/dist/migrations/logger.d.ts.map +1 -0
  35. package/dist/migrations/storage.d.ts.map +1 -1
  36. package/dist/migrations/users.d.ts +2 -2
  37. package/dist/migrations/users.d.ts.map +1 -1
  38. package/dist/query/helpers/join.d.ts.map +1 -1
  39. package/dist/query/helpers/populate/apply.d.ts.map +1 -1
  40. package/dist/query/helpers/where.d.ts +1 -0
  41. package/dist/query/helpers/where.d.ts.map +1 -1
  42. package/dist/schema/index.d.ts +0 -3
  43. package/dist/schema/index.d.ts.map +1 -1
  44. package/dist/transaction-context.d.ts.map +1 -1
  45. package/dist/types/index.d.ts +3 -0
  46. package/dist/types/index.d.ts.map +1 -1
  47. package/dist/utils/identifiers/hash.d.ts +31 -0
  48. package/dist/utils/identifiers/hash.d.ts.map +1 -0
  49. package/dist/utils/identifiers/index.d.ts +107 -47
  50. package/dist/utils/identifiers/index.d.ts.map +1 -1
  51. package/dist/utils/identifiers/types.d.ts +27 -0
  52. package/dist/utils/identifiers/types.d.ts.map +1 -0
  53. package/dist/validations/relations/bidirectional.d.ts.map +1 -1
  54. package/package.json +11 -8
  55. package/dist/utils/identifiers/shortener.d.ts +0 -73
  56. package/dist/utils/identifiers/shortener.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -7,13 +7,14 @@ const _ = require("lodash/fp");
7
7
  const crypto = require("crypto");
8
8
  const crypto$1 = require("node:crypto");
9
9
  const dateFns = require("date-fns");
10
- const utils$1 = require("@strapi/utils");
10
+ const utils = require("@strapi/utils");
11
11
  const KnexBuilder = require("knex/lib/query/querybuilder");
12
12
  const KnexRaw = require("knex/lib/raw");
13
13
  const stream = require("stream");
14
14
  const node_async_hooks = require("node:async_hooks");
15
15
  const _$1 = require("lodash");
16
16
  const umzug = require("umzug");
17
+ const cuid2 = require("@paralleldrive/cuid2");
17
18
  const assert = require("assert");
18
19
  const knex = require("knex");
19
20
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
@@ -1007,7 +1008,7 @@ const getDialect = (db) => {
1007
1008
  const dialect = new constructor(db, dialectName);
1008
1009
  return dialect;
1009
1010
  };
1010
- const debug$1 = createDebug__default.default("strapi::database");
1011
+ const debug$2 = createDebug__default.default("strapi::database");
1011
1012
  const createSchemaBuilder = (db) => {
1012
1013
  const helpers2 = createHelpers(db);
1013
1014
  return {
@@ -1033,12 +1034,12 @@ const createSchemaBuilder = (db) => {
1033
1034
  */
1034
1035
  async createTables(tables, trx) {
1035
1036
  for (const table of tables) {
1036
- debug$1(`Creating table: ${table.name}`);
1037
+ debug$2(`Creating table: ${table.name}`);
1037
1038
  const schemaBuilder = this.getSchemaBuilder(trx);
1038
1039
  await helpers2.createTable(schemaBuilder, table);
1039
1040
  }
1040
1041
  for (const table of tables) {
1041
- debug$1(`Creating table foreign keys: ${table.name}`);
1042
+ debug$2(`Creating table foreign keys: ${table.name}`);
1042
1043
  const schemaBuilder = this.getSchemaBuilder(trx);
1043
1044
  await helpers2.createTableForeignKeys(schemaBuilder, table);
1044
1045
  }
@@ -1069,18 +1070,18 @@ const createSchemaBuilder = (db) => {
1069
1070
  await this.createTables(schemaDiff.tables.added, trx);
1070
1071
  if (forceMigration) {
1071
1072
  for (const table of schemaDiff.tables.removed) {
1072
- debug$1(`Removing table foreign keys: ${table.name}`);
1073
+ debug$2(`Removing table foreign keys: ${table.name}`);
1073
1074
  const schemaBuilder = this.getSchemaBuilder(trx);
1074
1075
  await helpers2.dropTableForeignKeys(schemaBuilder, table);
1075
1076
  }
1076
1077
  for (const table of schemaDiff.tables.removed) {
1077
- debug$1(`Removing table: ${table.name}`);
1078
+ debug$2(`Removing table: ${table.name}`);
1078
1079
  const schemaBuilder = this.getSchemaBuilder(trx);
1079
1080
  await helpers2.dropTable(schemaBuilder, table);
1080
1081
  }
1081
1082
  }
1082
1083
  for (const table of schemaDiff.tables.updated) {
1083
- debug$1(`Updating table: ${table.name}`);
1084
+ debug$2(`Updating table: ${table.name}`);
1084
1085
  const schemaBuilder = this.getSchemaBuilder(trx);
1085
1086
  await helpers2.alterTable(schemaBuilder, table);
1086
1087
  }
@@ -1175,28 +1176,37 @@ const createHelpers = (db) => {
1175
1176
  };
1176
1177
  const alterTable = async (schemaBuilder, table) => {
1177
1178
  await schemaBuilder.alterTable(table.name, (tableBuilder) => {
1178
- for (const removedIndex of table.indexes.removed) {
1179
- debug$1(`Dropping index ${removedIndex.name}`);
1180
- dropIndex(tableBuilder, removedIndex);
1181
- }
1182
- for (const updateddIndex of table.indexes.updated) {
1183
- debug$1(`Dropping updated index ${updateddIndex.name}`);
1184
- dropIndex(tableBuilder, updateddIndex.object);
1185
- }
1186
1179
  for (const removedForeignKey of table.foreignKeys.removed) {
1187
- debug$1(`Dropping foreign key ${removedForeignKey.name}`);
1180
+ debug$2(`Dropping foreign key ${removedForeignKey.name} on ${table.name}`);
1188
1181
  dropForeignKey(tableBuilder, removedForeignKey);
1189
1182
  }
1190
1183
  for (const updatedForeignKey of table.foreignKeys.updated) {
1191
- debug$1(`Dropping updated foreign key ${updatedForeignKey.name}`);
1184
+ debug$2(`Dropping updated foreign key ${updatedForeignKey.name} on ${table.name}`);
1192
1185
  dropForeignKey(tableBuilder, updatedForeignKey.object);
1193
1186
  }
1194
1187
  for (const removedColumn of table.columns.removed) {
1195
- debug$1(`Dropping column ${removedColumn.name}`);
1188
+ debug$2(`Dropping column ${removedColumn.name} on ${table.name}`);
1196
1189
  dropColumn(tableBuilder, removedColumn);
1197
1190
  }
1191
+ const isMySQL = db.config.connection.client === "mysql";
1192
+ const ignoreForeignKeyNames = isMySQL ? [
1193
+ ...table.foreignKeys.removed.map((fk) => fk.name),
1194
+ ...table.foreignKeys.updated.map((fk) => fk.name)
1195
+ ] : [];
1196
+ for (const removedIndex of table.indexes.removed) {
1197
+ if (!ignoreForeignKeyNames.includes(removedIndex.name)) {
1198
+ debug$2(`Dropping index ${removedIndex.name} on ${table.name}`);
1199
+ dropIndex(tableBuilder, removedIndex);
1200
+ }
1201
+ }
1202
+ for (const updatedIndex of table.indexes.updated) {
1203
+ if (!ignoreForeignKeyNames.includes(updatedIndex.name)) {
1204
+ debug$2(`Dropping updated index ${updatedIndex.name} on ${table.name}`);
1205
+ dropIndex(tableBuilder, updatedIndex.object);
1206
+ }
1207
+ }
1198
1208
  for (const updatedColumn of table.columns.updated) {
1199
- debug$1(`Updating column ${updatedColumn.name}`);
1209
+ debug$2(`Updating column ${updatedColumn.name} on ${table.name}`);
1200
1210
  const { object } = updatedColumn;
1201
1211
  if (object.type === "increments") {
1202
1212
  createColumn2(tableBuilder, { ...object, type: "integer" }).alter();
@@ -1205,15 +1215,15 @@ const createHelpers = (db) => {
1205
1215
  }
1206
1216
  }
1207
1217
  for (const updatedForeignKey of table.foreignKeys.updated) {
1208
- debug$1(`Recreating updated foreign key ${updatedForeignKey.name}`);
1218
+ debug$2(`Recreating updated foreign key ${updatedForeignKey.name} on ${table.name}`);
1209
1219
  createForeignKey(tableBuilder, updatedForeignKey.object);
1210
1220
  }
1211
1221
  for (const updatedIndex of table.indexes.updated) {
1212
- debug$1(`Recreating updated index ${updatedIndex.name}`);
1222
+ debug$2(`Recreating updated index ${updatedIndex.name} on ${table.name}`);
1213
1223
  createIndex(tableBuilder, updatedIndex.object);
1214
1224
  }
1215
1225
  for (const addedColumn of table.columns.added) {
1216
- debug$1(`Creating column ${addedColumn.name}`);
1226
+ debug$2(`Creating column ${addedColumn.name} on ${table.name}`);
1217
1227
  if (addedColumn.type === "increments" && !db.dialect.canAddIncrements()) {
1218
1228
  tableBuilder.integer(addedColumn.name).unsigned();
1219
1229
  tableBuilder.primary([addedColumn.name]);
@@ -1222,11 +1232,11 @@ const createHelpers = (db) => {
1222
1232
  }
1223
1233
  }
1224
1234
  for (const addedForeignKey of table.foreignKeys.added) {
1225
- debug$1(`Creating foreign keys ${addedForeignKey.name}`);
1235
+ debug$2(`Creating foreign keys ${addedForeignKey.name} on ${table.name}`);
1226
1236
  createForeignKey(tableBuilder, addedForeignKey);
1227
1237
  }
1228
1238
  for (const addedIndex of table.indexes.added) {
1229
- debug$1(`Creating index ${addedIndex.name}`);
1239
+ debug$2(`Creating index ${addedIndex.name} on ${table.name}`);
1230
1240
  createIndex(tableBuilder, addedIndex);
1231
1241
  }
1232
1242
  });
@@ -1645,11 +1655,6 @@ const isScalar = (type) => SCALAR_TYPES.includes(type);
1645
1655
  const isRelation = (type) => type === "relation";
1646
1656
  const isScalarAttribute = (attribute) => isScalar(attribute.type);
1647
1657
  const isRelationalAttribute = (attribute) => isRelation(attribute.type);
1648
- const MAX_DB_IDENTIFIER_LENGTH = 0;
1649
- const HASH_LENGTH = 5;
1650
- const HASH_SEPARATOR = "";
1651
- const IDENTIFIER_SEPARATOR = "_";
1652
- const MIN_TOKEN_LENGTH = 3;
1653
1658
  function createHash(data, len) {
1654
1659
  if (!_.isInteger(len) || len <= 0) {
1655
1660
  throw new Error(`createHash length must be a positive integer, received ${len}`);
@@ -1657,201 +1662,334 @@ function createHash(data, len) {
1657
1662
  const hash = crypto__default$1.default.createHash("shake256", { outputLength: Math.ceil(len / 2) }).update(data);
1658
1663
  return hash.digest("hex").substring(0, len);
1659
1664
  }
1660
- function getShortenedName(name, len) {
1661
- if (!_.isInteger(len) || len <= 0) {
1662
- throw new Error(`tokenWithHash length must be a positive integer, received ${len}`);
1663
- }
1664
- if (name.length <= len) {
1665
- return name;
1666
- }
1667
- if (len < MIN_TOKEN_LENGTH + HASH_LENGTH) {
1668
- throw new Error(
1669
- `length for part of identifier too short, minimum is hash length (${HASH_LENGTH}) plus min token length (${MIN_TOKEN_LENGTH}), received ${len} for token ${name}`
1670
- );
1671
- }
1672
- const availableLength = len - HASH_LENGTH - HASH_SEPARATOR.length;
1673
- if (availableLength < MIN_TOKEN_LENGTH) {
1674
- throw new Error(
1675
- `length for part of identifier minimum is less than min token length (${MIN_TOKEN_LENGTH}), received ${len} for token ${name}`
1676
- );
1665
+ const IDENTIFIER_MAX_LENGTH = 55;
1666
+ class Identifiers {
1667
+ ID_COLUMN = "id";
1668
+ ORDER_COLUMN = "order";
1669
+ FIELD_COLUMN = "field";
1670
+ HASH_LENGTH = 5;
1671
+ HASH_SEPARATOR = "";
1672
+ // no separator is needed, we will just attach hash directly to shortened name
1673
+ IDENTIFIER_SEPARATOR = "_";
1674
+ MIN_TOKEN_LENGTH = 3;
1675
+ // the min characters required at the beginning of a name part
1676
+ // Fixed compression map for suffixes and prefixes
1677
+ #replacementMap = {
1678
+ links: "lnk",
1679
+ order_inv_fk: "oifk",
1680
+ order: "ord",
1681
+ morphs: "mph",
1682
+ index: "idx",
1683
+ inv_fk: "ifk",
1684
+ order_fk: "ofk",
1685
+ id_column_index: "idix",
1686
+ order_index: "oidx",
1687
+ unique: "uq",
1688
+ primary: "pk"
1689
+ };
1690
+ #options;
1691
+ constructor(options) {
1692
+ this.#options = options;
1677
1693
  }
1678
- return `${name.substring(0, availableLength)}${HASH_SEPARATOR}${createHash(name, HASH_LENGTH)}`;
1679
- }
1680
- function getNameFromTokens(nameTokens, maxLength = MAX_DB_IDENTIFIER_LENGTH) {
1681
- if (!_.isInteger(maxLength) || maxLength < 0) {
1682
- throw new Error("maxLength must be a positive integer or 0 (for unlimited length)");
1694
+ get replacementMap() {
1695
+ return this.#replacementMap;
1683
1696
  }
1684
- const fullLengthName = nameTokens.map((token) => _.snakeCase(token.name)).join(IDENTIFIER_SEPARATOR);
1685
- if (fullLengthName.length <= maxLength || maxLength === 0) {
1686
- return fullLengthName;
1697
+ get options() {
1698
+ return this.#options;
1687
1699
  }
1688
- const [compressible, incompressible] = _.partition(
1689
- (token) => token.compressible,
1690
- nameTokens
1691
- );
1692
- const totalIncompressibleLength = _.sumBy((token) => token.name.length)(incompressible);
1693
- const totalSeparatorsLength = nameTokens.length * IDENTIFIER_SEPARATOR.length - 1;
1694
- const available = maxLength - totalIncompressibleLength - totalSeparatorsLength;
1695
- const availablePerToken = Math.floor(available / compressible.length);
1696
- if (totalIncompressibleLength + totalSeparatorsLength > maxLength || availablePerToken < MIN_TOKEN_LENGTH) {
1697
- throw new Error("Maximum length is too small to accommodate all tokens");
1698
- }
1699
- let surplus = available % compressible.length;
1700
- const minHashedLength = HASH_LENGTH + HASH_SEPARATOR.length + MIN_TOKEN_LENGTH;
1701
- const totalLength = nameTokens.reduce((total, token) => {
1702
- if (token.compressible) {
1703
- if (token.name.length < availablePerToken) {
1704
- return total + token.name.length;
1705
- }
1706
- return total + minHashedLength;
1707
- }
1708
- return total + token.name.length;
1709
- }, nameTokens.length * IDENTIFIER_SEPARATOR.length - 1);
1710
- if (maxLength < totalLength) {
1711
- throw new Error("Maximum length is too small to accommodate all tokens");
1712
- }
1713
- let deficits = [];
1714
- compressible.forEach((token) => {
1715
- const actualLength = token.name.length;
1716
- if (actualLength < availablePerToken) {
1717
- surplus += availablePerToken - actualLength;
1718
- token.allocatedLength = actualLength;
1719
- } else {
1720
- token.allocatedLength = availablePerToken;
1721
- deficits.push(token);
1700
+ mapshortNames = (name) => {
1701
+ if (name in this.replacementMap) {
1702
+ return this.replacementMap[name];
1722
1703
  }
1723
- });
1724
- function filterAndIncreaseLength(token) {
1725
- if (token.allocatedLength < token.name.length && surplus > 0) {
1726
- token.allocatedLength += 1;
1727
- surplus -= 1;
1728
- return token.allocatedLength < token.name.length;
1704
+ return void 0;
1705
+ };
1706
+ // Generic name handler that must be used by all helper functions
1707
+ /**
1708
+ * TODO: we should be requiring snake_case inputs for all names here, but we
1709
+ * aren't and it will require some refactoring to make it work. Currently if
1710
+ * we get names 'myModel' and 'my_model' they would be converted to the same
1711
+ * final string my_model which generally works but is not entirely safe
1712
+ * */
1713
+ getName = (names, options) => {
1714
+ const tokens = ___default.default.castArray(names).map((name) => {
1715
+ return {
1716
+ name,
1717
+ compressible: true
1718
+ };
1719
+ });
1720
+ if (options?.suffix) {
1721
+ tokens.push({
1722
+ name: options.suffix,
1723
+ compressible: false,
1724
+ shortName: this.mapshortNames(options.suffix)
1725
+ });
1729
1726
  }
1730
- return false;
1731
- }
1732
- let previousSurplus = surplus + 1;
1733
- while (surplus > 0 && deficits.length > 0) {
1734
- deficits = deficits.filter((token) => filterAndIncreaseLength(token));
1735
- if (surplus === previousSurplus) {
1736
- break;
1727
+ if (options?.prefix) {
1728
+ tokens.unshift({
1729
+ name: options.prefix,
1730
+ compressible: false,
1731
+ shortName: this.mapshortNames(options.prefix)
1732
+ });
1737
1733
  }
1738
- previousSurplus = surplus;
1739
- }
1740
- const shortenedName = nameTokens.map((token) => {
1741
- if (token.compressible && "allocatedLength" in token && token.allocatedLength !== void 0) {
1742
- return getShortenedName(token.name, token.allocatedLength);
1734
+ return this.getNameFromTokens(tokens);
1735
+ };
1736
+ /**
1737
+ * TABLES
1738
+ */
1739
+ getTableName = (name, options) => {
1740
+ return this.getName(name, options);
1741
+ };
1742
+ getJoinTableName = (collectionName, attributeName, options) => {
1743
+ return this.getName([collectionName, attributeName], {
1744
+ suffix: "links",
1745
+ ...options
1746
+ });
1747
+ };
1748
+ getMorphTableName = (collectionName, attributeName, options) => {
1749
+ return this.getName([_.snakeCase(collectionName), _.snakeCase(attributeName)], {
1750
+ suffix: "morphs",
1751
+ ...options
1752
+ });
1753
+ };
1754
+ /**
1755
+ * COLUMNS
1756
+ */
1757
+ getColumnName = (attributeName, options) => {
1758
+ return this.getName(attributeName, options);
1759
+ };
1760
+ getJoinColumnAttributeIdName = (attributeName, options) => {
1761
+ return this.getName(attributeName, { suffix: "id", ...options });
1762
+ };
1763
+ getInverseJoinColumnAttributeIdName = (attributeName, options) => {
1764
+ return this.getName(_.snakeCase(attributeName), { suffix: "id", prefix: "inv", ...options });
1765
+ };
1766
+ getOrderColumnName = (singularName, options) => {
1767
+ return this.getName(singularName, { suffix: "order", ...options });
1768
+ };
1769
+ getInverseOrderColumnName = (singularName, options) => {
1770
+ return this.getName(singularName, {
1771
+ suffix: "order",
1772
+ prefix: "inv",
1773
+ ...options
1774
+ });
1775
+ };
1776
+ /**
1777
+ * Morph Join Tables
1778
+ */
1779
+ getMorphColumnJoinTableIdName = (singularName, options) => {
1780
+ return this.getName(_.snakeCase(singularName), { suffix: "id", ...options });
1781
+ };
1782
+ getMorphColumnAttributeIdName = (attributeName, options) => {
1783
+ return this.getName(_.snakeCase(attributeName), { suffix: "id", ...options });
1784
+ };
1785
+ getMorphColumnTypeName = (attributeName, options) => {
1786
+ return this.getName(_.snakeCase(attributeName), { suffix: "type", ...options });
1787
+ };
1788
+ /**
1789
+ * INDEXES
1790
+ * Note that these methods are generally used to reference full table names + attribute(s), which
1791
+ * may already be shortened strings rather than individual parts.
1792
+ * That is fine and expected to compress the previously incompressible parts of those strings,
1793
+ * because in these cases the relevant information is the table name and we can't really do
1794
+ * any better; shortening the individual parts again might make it even more confusing.
1795
+ *
1796
+ * So for example, the fk for the table `mytable_myattr4567d_localizations` will become
1797
+ * mytable_myattr4567d_loc63bf2_fk
1798
+ */
1799
+ // base index types
1800
+ getIndexName = (names, options) => {
1801
+ return this.getName(names, { suffix: "index", ...options });
1802
+ };
1803
+ getFkIndexName = (names, options) => {
1804
+ return this.getName(names, { suffix: "fk", ...options });
1805
+ };
1806
+ getUniqueIndexName = (names, options) => {
1807
+ return this.getName(names, { suffix: "unique", ...options });
1808
+ };
1809
+ getPrimaryIndexName = (names, options) => {
1810
+ return this.getName(names, { suffix: "primary", ...options });
1811
+ };
1812
+ // custom index types
1813
+ getInverseFkIndexName = (names, options) => {
1814
+ return this.getName(names, { suffix: "inv_fk", ...options });
1815
+ };
1816
+ getOrderFkIndexName = (names, options) => {
1817
+ return this.getName(names, { suffix: "order_fk", ...options });
1818
+ };
1819
+ getOrderInverseFkIndexName = (names, options) => {
1820
+ return this.getName(names, { suffix: "order_inv_fk", ...options });
1821
+ };
1822
+ getIdColumnIndexName = (names, options) => {
1823
+ return this.getName(names, { suffix: "id_column_index", ...options });
1824
+ };
1825
+ getOrderIndexName = (names, options) => {
1826
+ return this.getName(names, { suffix: "order_index", ...options });
1827
+ };
1828
+ /**
1829
+ * Generates a string with a max length, appending a hash at the end if necessary to keep it unique
1830
+ *
1831
+ * @example
1832
+ * // if we have strings such as "longstring1" and "longstring2" with a max length of 9,
1833
+ * // we don't want to end up with "longstrin" and "longstrin"
1834
+ * // we want something such as "longs0b23" and "longs953f"
1835
+ * const token1 = generateToken("longstring1", 9); // "longs0b23"
1836
+ * const token2 = generateToken("longstring2", 9); // "longs953f"
1837
+ *
1838
+ * @param name - The base name
1839
+ * @param len - The desired length of the token.
1840
+ * @returns The generated token with hash.
1841
+ * @throws Error if the length is not a positive integer, or if the length is too short for the token.
1842
+ * @internal
1843
+ */
1844
+ getShortenedName = (name, len) => {
1845
+ if (!_.isInteger(len) || len <= 0) {
1846
+ throw new Error(`tokenWithHash length must be a positive integer, received ${len}`);
1743
1847
  }
1744
- return token.name;
1745
- }).join(IDENTIFIER_SEPARATOR);
1746
- if (shortenedName.length > maxLength) {
1747
- throw new Error(
1748
- `name shortening failed to generate a name of the correct maxLength; name ${shortenedName}`
1848
+ if (name.length <= len) {
1849
+ return name;
1850
+ }
1851
+ if (len < this.MIN_TOKEN_LENGTH + this.HASH_LENGTH) {
1852
+ throw new Error(
1853
+ `length for part of identifier too short, minimum is hash length (${this.HASH_LENGTH}) plus min token length (${this.MIN_TOKEN_LENGTH}), received ${len} for token ${name}`
1854
+ );
1855
+ }
1856
+ const availableLength = len - this.HASH_LENGTH - this.HASH_SEPARATOR.length;
1857
+ if (availableLength < this.MIN_TOKEN_LENGTH) {
1858
+ throw new Error(
1859
+ `length for part of identifier minimum is less than min token length (${this.MIN_TOKEN_LENGTH}), received ${len} for token ${name}`
1860
+ );
1861
+ }
1862
+ return `${name.substring(0, availableLength)}${this.HASH_SEPARATOR}${createHash(
1863
+ name,
1864
+ this.HASH_LENGTH
1865
+ )}`;
1866
+ };
1867
+ /**
1868
+ * Constructs a name from an array of name tokens within a specified maximum length. It ensures the final name does not exceed
1869
+ * this limit by selectively compressing tokens marked as compressible. If the name exceeds the maximum length and cannot be
1870
+ * compressed sufficiently, an error is thrown. This function supports dynamic adjustment of token lengths to fit within the
1871
+ * maxLength constraint (that is, it will always make use of all available space), while also ensuring the preservation of
1872
+ * incompressible tokens.
1873
+ * @internal
1874
+ */
1875
+ getNameFromTokens = (nameTokens) => {
1876
+ const { maxLength } = this.options;
1877
+ if (!_.isInteger(maxLength) || maxLength < 0) {
1878
+ throw new Error("maxLength must be a positive integer or 0 (for unlimited length)");
1879
+ }
1880
+ const unshortenedName = nameTokens.map((token) => {
1881
+ return token.name;
1882
+ }).join(this.IDENTIFIER_SEPARATOR);
1883
+ if (maxLength === 0) {
1884
+ this.setUnshortenedName(unshortenedName, unshortenedName);
1885
+ return unshortenedName;
1886
+ }
1887
+ const fullLengthName = nameTokens.map((token) => {
1888
+ if (token.compressible) {
1889
+ return token.name;
1890
+ }
1891
+ return token.shortName ?? token.name;
1892
+ }).join(this.IDENTIFIER_SEPARATOR);
1893
+ if (fullLengthName.length <= maxLength) {
1894
+ this.setUnshortenedName(fullLengthName, unshortenedName);
1895
+ return fullLengthName;
1896
+ }
1897
+ const [compressible, incompressible] = _.partition(
1898
+ (token) => token.compressible,
1899
+ nameTokens
1749
1900
  );
1750
- }
1751
- return shortenedName;
1901
+ const totalIncompressibleLength = _.sumBy(
1902
+ (token) => token.compressible === false && token.shortName !== void 0 ? token.shortName.length : token.name.length
1903
+ )(incompressible);
1904
+ const totalSeparatorsLength = nameTokens.length * this.IDENTIFIER_SEPARATOR.length - 1;
1905
+ const available = maxLength - totalIncompressibleLength - totalSeparatorsLength;
1906
+ const availablePerToken = Math.floor(available / compressible.length);
1907
+ if (totalIncompressibleLength + totalSeparatorsLength > maxLength || availablePerToken < this.MIN_TOKEN_LENGTH) {
1908
+ throw new Error("Maximum length is too small to accommodate all tokens");
1909
+ }
1910
+ let surplus = available % compressible.length;
1911
+ const minHashedLength = this.HASH_LENGTH + this.HASH_SEPARATOR.length + this.MIN_TOKEN_LENGTH;
1912
+ const totalLength = nameTokens.reduce(
1913
+ (total, token) => {
1914
+ if (token.compressible) {
1915
+ if (token.name.length < availablePerToken) {
1916
+ return total + token.name.length;
1917
+ }
1918
+ return total + minHashedLength;
1919
+ }
1920
+ const tokenName = token.shortName ?? token.name;
1921
+ return total + tokenName.length;
1922
+ },
1923
+ nameTokens.length * this.IDENTIFIER_SEPARATOR.length - 1
1924
+ );
1925
+ if (maxLength < totalLength) {
1926
+ throw new Error("Maximum length is too small to accommodate all tokens");
1927
+ }
1928
+ let deficits = [];
1929
+ compressible.forEach((token) => {
1930
+ const actualLength = token.name.length;
1931
+ if (actualLength < availablePerToken) {
1932
+ surplus += availablePerToken - actualLength;
1933
+ token.allocatedLength = actualLength;
1934
+ } else {
1935
+ token.allocatedLength = availablePerToken;
1936
+ deficits.push(token);
1937
+ }
1938
+ });
1939
+ function filterAndIncreaseLength(token) {
1940
+ if (token.allocatedLength < token.name.length && surplus > 0) {
1941
+ token.allocatedLength += 1;
1942
+ surplus -= 1;
1943
+ return token.allocatedLength < token.name.length;
1944
+ }
1945
+ return false;
1946
+ }
1947
+ let previousSurplus = surplus + 1;
1948
+ while (surplus > 0 && deficits.length > 0) {
1949
+ deficits = deficits.filter((token) => filterAndIncreaseLength(token));
1950
+ if (surplus === previousSurplus) {
1951
+ break;
1952
+ }
1953
+ previousSurplus = surplus;
1954
+ }
1955
+ const shortenedName = nameTokens.map((token) => {
1956
+ if (token.compressible && "allocatedLength" in token && token.allocatedLength !== void 0) {
1957
+ return this.getShortenedName(token.name, token.allocatedLength);
1958
+ }
1959
+ if (token.compressible === false && token.shortName) {
1960
+ return token.shortName;
1961
+ }
1962
+ return token.name;
1963
+ }).join(this.IDENTIFIER_SEPARATOR);
1964
+ if (shortenedName.length > maxLength) {
1965
+ throw new Error(
1966
+ `name shortening failed to generate a name of the correct maxLength; name ${shortenedName}`
1967
+ );
1968
+ }
1969
+ this.setUnshortenedName(shortenedName, unshortenedName);
1970
+ return shortenedName;
1971
+ };
1972
+ // We need to be able to find the full-length name for any shortened name, primarily for migration purposes
1973
+ // Therefore we store every name that passes through so we can retrieve the original later
1974
+ nameMap = /* @__PURE__ */ new Map();
1975
+ getUnshortenedName = (shortName) => {
1976
+ return this.nameMap.get(this.serializeKey(shortName)) ?? shortName;
1977
+ };
1978
+ setUnshortenedName = (shortName, fullName) => {
1979
+ if (this.nameMap.get(this.serializeKey(shortName)) && shortName === fullName) {
1980
+ return;
1981
+ }
1982
+ this.nameMap.set(this.serializeKey(shortName), fullName);
1983
+ };
1984
+ serializeKey = (shortName) => {
1985
+ return `${shortName}.${this.options.maxLength}`;
1986
+ };
1752
1987
  }
1753
- const ID_COLUMN = "id";
1754
- const ORDER_COLUMN = "order";
1755
- const FIELD_COLUMN = "field";
1756
- const getName = (names, options) => {
1757
- const tokens = ___default.default.castArray(names).map((name) => {
1758
- return {
1759
- name,
1760
- compressible: true
1761
- };
1762
- });
1763
- if (options?.suffix) {
1764
- tokens.push({ name: options.suffix, compressible: false });
1765
- }
1766
- if (options?.prefix) {
1767
- tokens.unshift({ name: options.prefix, compressible: false });
1768
- }
1769
- const maxLength = options?.maxLength ?? MAX_DB_IDENTIFIER_LENGTH;
1770
- return getNameFromTokens(tokens, maxLength);
1771
- };
1772
- const getTableName = (name, options) => {
1773
- return getName(name, options);
1774
- };
1775
- const getJoinTableName = (collectionName, attributeName, options) => {
1776
- return getName([collectionName, attributeName], { suffix: "links", ...options });
1777
- };
1778
- const getMorphTableName = (collectionName, attributeName, options) => {
1779
- return getName([collectionName, attributeName], { suffix: "morphs", ...options });
1780
- };
1781
- const getColumnName = (attributeName, options) => {
1782
- return getName(attributeName, options);
1783
- };
1784
- const getJoinColumnAttributeIdName = (attributeName, options) => {
1785
- return getName(attributeName, { suffix: "id", ...options });
1786
- };
1787
- const getInverseJoinColumnAttributeIdName = (attributeName, options) => {
1788
- return getName(attributeName, { suffix: "id", prefix: "inv", ...options });
1789
- };
1790
- const getOrderColumnName = (singularName, options) => {
1791
- return getName(singularName, { suffix: "order", ...options });
1792
- };
1793
- const getInverseOrderColumnName = (singularName, options) => {
1794
- return getName(singularName, { suffix: "order", prefix: "inv", ...options });
1795
- };
1796
- const getMorphColumnJoinTableIdName = (singularName, options) => {
1797
- return getName(singularName, { suffix: "id", ...options });
1798
- };
1799
- const getMorphColumnAttributeIdName = (attributeName, options) => {
1800
- return getName(attributeName, { suffix: "id", ...options });
1801
- };
1802
- const getMorphColumnTypeName = (attributeName, options) => {
1803
- return getName(attributeName, { suffix: "type", ...options });
1804
- };
1805
- const getIndexName = (names, options) => {
1806
- return getName(names, { suffix: "index", ...options });
1807
- };
1808
- const getFkIndexName = (names, options) => {
1809
- return getName(names, { suffix: "fk", ...options });
1810
- };
1811
- const getInverseFkIndexName = (names, options) => {
1812
- return getName(names, { suffix: "inv_fk", ...options });
1813
- };
1814
- const getOrderFkIndexName = (names, options) => {
1815
- return getName(names, { suffix: "order_fk", ...options });
1816
- };
1817
- const getOrderInverseFkIndexName = (names, options) => {
1818
- return getName(names, { suffix: "order_inv_fk", ...options });
1819
- };
1820
- const getUniqueIndexName = (names, options) => {
1821
- return getName(names, { suffix: "unique", ...options });
1822
- };
1823
- const getPrimaryIndexName = (names) => {
1824
- return getName(names, { suffix: "primary" });
1825
- };
1826
- const identifiers = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1827
- __proto__: null,
1828
- FIELD_COLUMN,
1829
- ID_COLUMN,
1830
- ORDER_COLUMN,
1831
- getColumnName,
1832
- getFkIndexName,
1833
- getIndexName,
1834
- getInverseFkIndexName,
1835
- getInverseJoinColumnAttributeIdName,
1836
- getInverseOrderColumnName,
1837
- getJoinColumnAttributeIdName,
1838
- getJoinTableName,
1839
- getMorphColumnAttributeIdName,
1840
- getMorphColumnJoinTableIdName,
1841
- getMorphColumnTypeName,
1842
- getMorphTableName,
1843
- getName,
1844
- getOrderColumnName,
1845
- getOrderFkIndexName,
1846
- getOrderInverseFkIndexName,
1847
- getPrimaryIndexName,
1848
- getTableName,
1849
- getUniqueIndexName
1850
- }, Symbol.toStringTag, { value: "Module" }));
1988
+ const identifiers = new Identifiers({ maxLength: IDENTIFIER_MAX_LENGTH });
1851
1989
  const createColumn = (name, attribute) => {
1852
1990
  const { type, args = [], ...opts } = getColumnType(attribute);
1853
1991
  return {
1854
- name,
1992
+ name: identifiers.getName(name),
1855
1993
  type,
1856
1994
  args,
1857
1995
  defaultTo: null,
@@ -1873,8 +2011,8 @@ const createTable = (meta) => {
1873
2011
  if (attribute.type === "relation") {
1874
2012
  if ("morphColumn" in attribute && attribute.morphColumn && attribute.owner) {
1875
2013
  const { idColumn, typeColumn } = attribute.morphColumn;
1876
- const idColumnName = getName(idColumn.name);
1877
- const typeColumnName = getName(typeColumn.name);
2014
+ const idColumnName = identifiers.getName(idColumn.name);
2015
+ const typeColumnName = identifiers.getName(typeColumn.name);
1878
2016
  table.columns.push(
1879
2017
  createColumn(idColumnName, {
1880
2018
  type: "integer",
@@ -1891,7 +2029,7 @@ const createTable = (meta) => {
1891
2029
  referencedTable,
1892
2030
  columnType = "integer"
1893
2031
  } = attribute.joinColumn;
1894
- const columnName = getName(columnNameFull);
2032
+ const columnName = identifiers.getName(columnNameFull);
1895
2033
  const column = createColumn(columnName, {
1896
2034
  // TODO: find the column type automatically, or allow passing all the column params
1897
2035
  type: columnType,
@@ -1900,7 +2038,7 @@ const createTable = (meta) => {
1900
2038
  }
1901
2039
  });
1902
2040
  table.columns.push(column);
1903
- const fkName = getFkIndexName([table.name, columnName]);
2041
+ const fkName = identifiers.getFkIndexName([table.name, columnName]);
1904
2042
  table.foreignKeys.push({
1905
2043
  name: fkName,
1906
2044
  columns: [column.name],
@@ -1915,19 +2053,19 @@ const createTable = (meta) => {
1915
2053
  });
1916
2054
  }
1917
2055
  } else if (isScalarAttribute(attribute)) {
1918
- const columnName = getName(attribute.columnName || key);
2056
+ const columnName = identifiers.getName(attribute.columnName || key);
1919
2057
  const column = createColumn(columnName, attribute);
1920
2058
  if (column.unique) {
1921
2059
  table.indexes.push({
1922
2060
  type: "unique",
1923
- name: getUniqueIndexName([table.name, column.name]),
2061
+ name: identifiers.getUniqueIndexName([table.name, column.name]),
1924
2062
  columns: [columnName]
1925
2063
  });
1926
2064
  }
1927
2065
  if (column.primary) {
1928
2066
  table.indexes.push({
1929
2067
  type: "primary",
1930
- name: getPrimaryIndexName([table.name, column.name]),
2068
+ name: identifiers.getPrimaryIndexName([table.name, column.name]),
1931
2069
  columns: [columnName]
1932
2070
  });
1933
2071
  }
@@ -2027,12 +2165,13 @@ const metadataToSchema = (metadata) => {
2027
2165
  });
2028
2166
  return schema;
2029
2167
  };
2030
- const debug = createDebug__default.default("strapi::database");
2168
+ const debug$1 = createDebug__default.default("strapi::database");
2031
2169
  const createSchemaProvider = (db) => {
2032
2170
  const state = {};
2033
2171
  return {
2034
2172
  get schema() {
2035
2173
  if (!state.schema) {
2174
+ debug$1("Converting metadata to database schema");
2036
2175
  state.schema = metadataToSchema(db.metadata);
2037
2176
  }
2038
2177
  return state.schema;
@@ -2044,7 +2183,7 @@ const createSchemaProvider = (db) => {
2044
2183
  * Drops the database schema
2045
2184
  */
2046
2185
  async drop() {
2047
- debug("Dropping database schema");
2186
+ debug$1("Dropping database schema");
2048
2187
  const DBSchema = await db.dialect.schemaInspector.getSchema();
2049
2188
  await this.builder.dropSchema(DBSchema);
2050
2189
  },
@@ -2052,19 +2191,19 @@ const createSchemaProvider = (db) => {
2052
2191
  * Creates the database schema
2053
2192
  */
2054
2193
  async create() {
2055
- debug("Created database schema");
2194
+ debug$1("Created database schema");
2056
2195
  await this.builder.createSchema(this.schema);
2057
2196
  },
2058
2197
  /**
2059
2198
  * Resets the database schema
2060
2199
  */
2061
2200
  async reset() {
2062
- debug("Resetting database schema");
2201
+ debug$1("Resetting database schema");
2063
2202
  await this.drop();
2064
2203
  await this.create();
2065
2204
  },
2066
2205
  async syncSchema() {
2067
- debug("Synchronizing database schema");
2206
+ debug$1("Synchronizing database schema");
2068
2207
  const DBSchema = await db.dialect.schemaInspector.getSchema();
2069
2208
  const { status, diff } = await this.schemaDiff.diff(DBSchema, this.schema);
2070
2209
  if (status === "CHANGED") {
@@ -2077,28 +2216,28 @@ const createSchemaProvider = (db) => {
2077
2216
  // TODO: Allow keeping extra indexes / extra tables / extra columns (globally or on a per table basis)
2078
2217
  async sync() {
2079
2218
  if (await db.migrations.shouldRun()) {
2080
- debug("Found migrations to run");
2219
+ debug$1("Found migrations to run");
2081
2220
  await db.migrations.up();
2082
2221
  return this.syncSchema();
2083
2222
  }
2084
2223
  const oldSchema = await this.schemaStorage.read();
2085
2224
  if (!oldSchema) {
2086
- debug("Schema not persisted yet");
2225
+ debug$1("Schema not persisted yet");
2087
2226
  return this.syncSchema();
2088
2227
  }
2089
2228
  const { hash: oldHash } = oldSchema;
2090
2229
  const hash = await this.schemaStorage.hashSchema(this.schema);
2091
2230
  if (oldHash !== hash) {
2092
- debug("Schema changed");
2231
+ debug$1("Schema changed");
2093
2232
  return this.syncSchema();
2094
2233
  }
2095
- debug("Schema unchanged");
2234
+ debug$1("Schema unchanged");
2096
2235
  }
2097
2236
  };
2098
2237
  };
2099
- const ID = ID_COLUMN;
2100
- const ORDER = ORDER_COLUMN;
2101
- const FIELD = FIELD_COLUMN;
2238
+ const ID = identifiers.ID_COLUMN;
2239
+ const ORDER = identifiers.ORDER_COLUMN;
2240
+ const FIELD = identifiers.FIELD_COLUMN;
2102
2241
  const hasInversedBy = (attr) => "inversedBy" in attr;
2103
2242
  const hasMappedBy = (attr) => "mappedBy" in attr;
2104
2243
  const isOneToAny = (attribute) => ["oneToOne", "oneToMany"].includes(attribute.relation);
@@ -2119,7 +2258,7 @@ const createOneToOne = (attributeName, attribute, meta, metadata) => {
2119
2258
  meta
2120
2259
  });
2121
2260
  } else {
2122
- createJoinColum(metadata, {
2261
+ createJoinColumn(metadata, {
2123
2262
  attribute,
2124
2263
  attributeName,
2125
2264
  meta
@@ -2149,7 +2288,7 @@ const createManyToOne = (attributeName, attribute, meta, metadata) => {
2149
2288
  meta
2150
2289
  });
2151
2290
  } else {
2152
- createJoinColum(metadata, {
2291
+ createJoinColumn(metadata, {
2153
2292
  attribute,
2154
2293
  attributeName,
2155
2294
  meta
@@ -2166,15 +2305,11 @@ const createManyToMany = (attributeName, attribute, meta, metadata) => {
2166
2305
  }
2167
2306
  };
2168
2307
  const createMorphToOne = (attributeName, attribute) => {
2169
- const idColumnName = getJoinColumnAttributeIdName("target");
2170
- const typeColumnName = getMorphColumnTypeName("target");
2171
- if ("morphColumn" in attribute && attribute.morphColumn) {
2172
- return;
2173
- }
2308
+ const idColumnName = identifiers.getJoinColumnAttributeIdName("target");
2309
+ const typeColumnName = identifiers.getMorphColumnTypeName("target");
2174
2310
  Object.assign(attribute, {
2175
2311
  owner: true,
2176
- morphColumn: {
2177
- // TODO: add referenced column
2312
+ morphColumn: attribute.morphColumn ?? {
2178
2313
  typeColumn: {
2179
2314
  name: typeColumnName
2180
2315
  },
@@ -2189,11 +2324,11 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => {
2189
2324
  if ("joinTable" in attribute && attribute.joinTable) {
2190
2325
  return;
2191
2326
  }
2192
- const joinTableName = getMorphTableName(meta.tableName, attributeName);
2193
- const joinColumnName = getMorphColumnJoinTableIdName(meta.singularName);
2194
- const idColumnName = getMorphColumnAttributeIdName(attributeName);
2195
- const typeColumnName = getMorphColumnTypeName(attributeName);
2196
- const fkIndexName = getFkIndexName(joinTableName);
2327
+ const joinTableName = identifiers.getMorphTableName(meta.tableName, attributeName);
2328
+ const joinColumnName = identifiers.getMorphColumnJoinTableIdName(_.snakeCase(meta.singularName));
2329
+ const idColumnName = identifiers.getMorphColumnAttributeIdName(attributeName);
2330
+ const typeColumnName = identifiers.getMorphColumnTypeName(attributeName);
2331
+ const fkIndexName = identifiers.getFkIndexName(joinTableName);
2197
2332
  metadata.add({
2198
2333
  singularName: joinTableName,
2199
2334
  uid: joinTableName,
@@ -2206,7 +2341,9 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => {
2206
2341
  type: "integer",
2207
2342
  column: {
2208
2343
  unsigned: true
2209
- }
2344
+ },
2345
+ // This must be set explicitly so that it is used instead of shortening the attribute name, which is already shortened
2346
+ columnName: joinColumnName
2210
2347
  },
2211
2348
  [idColumnName]: {
2212
2349
  type: "integer",
@@ -2233,11 +2370,11 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => {
2233
2370
  columns: [joinColumnName]
2234
2371
  },
2235
2372
  {
2236
- name: `${joinTableName}_order_index`,
2373
+ name: identifiers.getOrderIndexName(joinTableName),
2237
2374
  columns: [ORDER]
2238
2375
  },
2239
2376
  {
2240
- name: `${joinTableName}_id_column_index`,
2377
+ name: identifiers.getIdColumnIndexName(joinTableName),
2241
2378
  columns: [idColumnName]
2242
2379
  }
2243
2380
  ],
@@ -2246,7 +2383,7 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => {
2246
2383
  name: fkIndexName,
2247
2384
  columns: [joinColumnName],
2248
2385
  referencedColumns: [ID],
2249
- referencedTable: getTableName(meta.tableName),
2386
+ referencedTable: meta.tableName,
2250
2387
  onDelete: "CASCADE"
2251
2388
  }
2252
2389
  ],
@@ -2293,12 +2430,12 @@ const createMorphMany = (attributeName, attribute, meta, metadata) => {
2293
2430
  throw new Error(`Morph target attribute not found. Looking for ${attribute.morphBy}`);
2294
2431
  }
2295
2432
  };
2296
- const createJoinColum = (metadata, { attribute, attributeName }) => {
2433
+ const createJoinColumn = (metadata, { attribute, attributeName }) => {
2297
2434
  const targetMeta = metadata.get(attribute.target);
2298
2435
  if (!targetMeta) {
2299
2436
  throw new Error(`Unknown target ${attribute.target}`);
2300
2437
  }
2301
- const joinColumnName = ___default.default.snakeCase(`${attributeName}_id`);
2438
+ const joinColumnName = identifiers.getJoinColumnAttributeIdName(_.snakeCase(attributeName));
2302
2439
  const joinColumn = {
2303
2440
  name: joinColumnName,
2304
2441
  referencedColumn: ID,
@@ -2313,7 +2450,7 @@ const createJoinColum = (metadata, { attribute, attributeName }) => {
2313
2450
  Object.assign(inverseAttribute, {
2314
2451
  joinColumn: {
2315
2452
  name: joinColumn.referencedColumn,
2316
- referencedColumn: joinColumn.name
2453
+ referencedColumn: joinColumnName
2317
2454
  }
2318
2455
  });
2319
2456
  }
@@ -2326,21 +2463,26 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
2326
2463
  if ("joinTable" in attribute && attribute.joinTable) {
2327
2464
  return;
2328
2465
  }
2329
- const joinTableName = getJoinTableName(meta.tableName, attributeName);
2330
- const joinColumnName = getJoinColumnAttributeIdName(meta.singularName);
2331
- let inverseJoinColumnName = getJoinColumnAttributeIdName(targetMeta.singularName);
2466
+ const joinTableName = identifiers.getJoinTableName(
2467
+ _.snakeCase(meta.tableName),
2468
+ _.snakeCase(attributeName)
2469
+ );
2470
+ const joinColumnName = identifiers.getJoinColumnAttributeIdName(_.snakeCase(meta.singularName));
2471
+ let inverseJoinColumnName = identifiers.getJoinColumnAttributeIdName(
2472
+ _.snakeCase(targetMeta.singularName)
2473
+ );
2332
2474
  if (joinColumnName === inverseJoinColumnName) {
2333
- inverseJoinColumnName = getInverseJoinColumnAttributeIdName(
2334
- targetMeta.singularName
2475
+ inverseJoinColumnName = identifiers.getInverseJoinColumnAttributeIdName(
2476
+ _.snakeCase(targetMeta.singularName)
2335
2477
  );
2336
2478
  }
2337
- const orderColumnName = getOrderColumnName(targetMeta.singularName);
2338
- let inverseOrderColumnName = getOrderColumnName(meta.singularName);
2479
+ const orderColumnName = identifiers.getOrderColumnName(_.snakeCase(targetMeta.singularName));
2480
+ let inverseOrderColumnName = identifiers.getOrderColumnName(_.snakeCase(meta.singularName));
2339
2481
  if (attribute.relation === "manyToMany" && orderColumnName === inverseOrderColumnName) {
2340
- inverseOrderColumnName = getInverseOrderColumnName(meta.singularName);
2482
+ inverseOrderColumnName = identifiers.getInverseOrderColumnName(_.snakeCase(meta.singularName));
2341
2483
  }
2342
- const fkIndexName = getFkIndexName(joinTableName);
2343
- const invFkIndexName = getInverseFkIndexName(joinTableName);
2484
+ const fkIndexName = identifiers.getFkIndexName(joinTableName);
2485
+ const invFkIndexName = identifiers.getInverseFkIndexName(joinTableName);
2344
2486
  const metadataSchema = {
2345
2487
  singularName: joinTableName,
2346
2488
  uid: joinTableName,
@@ -2353,13 +2495,17 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
2353
2495
  type: "integer",
2354
2496
  column: {
2355
2497
  unsigned: true
2356
- }
2498
+ },
2499
+ // This must be set explicitly so that it is used instead of shortening the attribute name, which is already shortened
2500
+ columnName: joinColumnName
2357
2501
  },
2358
2502
  [inverseJoinColumnName]: {
2359
2503
  type: "integer",
2360
2504
  column: {
2361
2505
  unsigned: true
2362
- }
2506
+ },
2507
+ // This must be set explicitly so that it is used instead of shortening the attribute name, which is already shortened
2508
+ columnName: inverseJoinColumnName
2363
2509
  }
2364
2510
  // TODO: add extra pivot attributes -> user should use an intermediate entity
2365
2511
  },
@@ -2373,7 +2519,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
2373
2519
  columns: [inverseJoinColumnName]
2374
2520
  },
2375
2521
  {
2376
- name: getUniqueIndexName(joinTableName),
2522
+ name: identifiers.getUniqueIndexName(joinTableName),
2377
2523
  columns: [joinColumnName, inverseJoinColumnName],
2378
2524
  type: "unique"
2379
2525
  }
@@ -2384,7 +2530,6 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
2384
2530
  columns: [joinColumnName],
2385
2531
  referencedColumns: [ID],
2386
2532
  referencedTable: meta.tableName,
2387
- // TODO: does this need to be wrapped or do we trust meta.tableName to be the right shortened version?
2388
2533
  onDelete: "CASCADE"
2389
2534
  },
2390
2535
  {
@@ -2392,7 +2537,6 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
2392
2537
  columns: [inverseJoinColumnName],
2393
2538
  referencedColumns: [ID],
2394
2539
  referencedTable: targetMeta.tableName,
2395
- // TODO: does this need to be wrapped or do we trust meta.tableName to be the right shortened version?
2396
2540
  onDelete: "CASCADE"
2397
2541
  }
2398
2542
  ],
@@ -2422,8 +2566,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
2422
2566
  }
2423
2567
  };
2424
2568
  metadataSchema.indexes.push({
2425
- name: getOrderFkIndexName(joinTableName),
2426
- // TODO: should we send joinTableName as parts?
2569
+ name: identifiers.getOrderFkIndexName(joinTableName),
2427
2570
  columns: [orderColumnName]
2428
2571
  });
2429
2572
  joinTable.orderColumnName = orderColumnName;
@@ -2438,7 +2581,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
2438
2581
  }
2439
2582
  };
2440
2583
  metadataSchema.indexes.push({
2441
- name: getOrderInverseFkIndexName(joinTableName),
2584
+ name: identifiers.getOrderInverseFkIndexName(joinTableName),
2442
2585
  columns: [inverseOrderColumnName]
2443
2586
  });
2444
2587
  joinTable.inverseOrderColumnName = inverseOrderColumnName;
@@ -2496,6 +2639,12 @@ const createRelation = (attributeName, attribute, meta, metadata) => {
2496
2639
  }
2497
2640
  };
2498
2641
  class Metadata extends Map {
2642
+ // TODO: we expose the global identifiers in this way so that in the future we can instantiate our own
2643
+ // However, it should NOT be done until all the methods used by metadata can be part of this metadata object
2644
+ // and access this one; currently they all access the global identifiers directly.
2645
+ get identifiers() {
2646
+ return identifiers;
2647
+ }
2499
2648
  get(key) {
2500
2649
  if (!super.has(key)) {
2501
2650
  throw new Error(`Metadata for "${key}" not found`);
@@ -2519,10 +2668,12 @@ class Metadata extends Map {
2519
2668
  seenTables.set(meta.tableName, true);
2520
2669
  }
2521
2670
  }
2522
- loadModels(models = []) {
2523
- for (const model of ___default.default.cloneDeep(models)) {
2671
+ loadModels(models) {
2672
+ for (const model of _.cloneDeep(models ?? [])) {
2673
+ const tableName = identifiers.getTableName(model.tableName);
2524
2674
  this.add({
2525
2675
  ...model,
2676
+ tableName,
2526
2677
  attributes: {
2527
2678
  ...model.attributes
2528
2679
  },
@@ -2563,10 +2714,13 @@ class Metadata extends Map {
2563
2714
  }
2564
2715
  }
2565
2716
  const createAttribute = (attributeName, attribute) => {
2566
- const columnName = getColumnName(attributeName);
2717
+ if ("columnName" in attribute && attribute.columnName) {
2718
+ return;
2719
+ }
2720
+ const columnName = identifiers.getColumnName(_.snakeCase(attributeName));
2567
2721
  Object.assign(attribute, { columnName });
2568
2722
  };
2569
- const createMetadata = (models = []) => {
2723
+ const createMetadata = (models) => {
2570
2724
  const metadata = new Metadata();
2571
2725
  if (models.length) {
2572
2726
  metadata.loadModels(models);
@@ -2917,11 +3071,54 @@ const createPivotJoin = (ctx, { alias, refAlias, joinTable, targetMeta }) => {
2917
3071
  return subAlias;
2918
3072
  };
2919
3073
  const createJoin = (ctx, { alias, refAlias, attributeName, attribute }) => {
2920
- const { db, qb } = ctx;
3074
+ const { db, qb, uid } = ctx;
2921
3075
  if (attribute.type !== "relation") {
2922
3076
  throw new Error(`Cannot join on non relational field ${attributeName}`);
2923
3077
  }
2924
3078
  const targetMeta = db.metadata.get(attribute.target);
3079
+ if (["morphOne", "morphMany"].includes(attribute.relation)) {
3080
+ const targetAttribute = targetMeta.attributes[attribute.morphBy];
3081
+ const { joinTable: joinTable2, morphColumn } = targetAttribute;
3082
+ if (morphColumn) {
3083
+ const subAlias = refAlias || qb.getAlias();
3084
+ qb.join({
3085
+ alias: subAlias,
3086
+ referencedTable: targetMeta.tableName,
3087
+ referencedColumn: morphColumn.idColumn.name,
3088
+ rootColumn: morphColumn.idColumn.referencedColumn,
3089
+ rootTable: alias,
3090
+ on: {
3091
+ [morphColumn.typeColumn.name]: uid,
3092
+ ...morphColumn.on
3093
+ }
3094
+ });
3095
+ return subAlias;
3096
+ }
3097
+ if (joinTable2) {
3098
+ const joinAlias = qb.getAlias();
3099
+ qb.join({
3100
+ alias: joinAlias,
3101
+ referencedTable: joinTable2.name,
3102
+ referencedColumn: joinTable2.morphColumn.idColumn.name,
3103
+ rootColumn: joinTable2.morphColumn.idColumn.referencedColumn,
3104
+ rootTable: alias,
3105
+ on: {
3106
+ [joinTable2.morphColumn.typeColumn.name]: uid,
3107
+ field: attributeName
3108
+ }
3109
+ });
3110
+ const subAlias = refAlias || qb.getAlias();
3111
+ qb.join({
3112
+ alias: subAlias,
3113
+ referencedTable: targetMeta.tableName,
3114
+ referencedColumn: joinTable2.joinColumn.referencedColumn,
3115
+ rootColumn: joinTable2.joinColumn.name,
3116
+ rootTable: joinAlias
3117
+ });
3118
+ return subAlias;
3119
+ }
3120
+ return alias;
3121
+ }
2925
3122
  const { joinColumn } = attribute;
2926
3123
  if (joinColumn) {
2927
3124
  const subAlias = refAlias || qb.getAlias();
@@ -3015,6 +3212,7 @@ const processOrderBy = (orderBy, ctx) => {
3015
3212
  }
3016
3213
  throw new Error("Invalid orderBy syntax");
3017
3214
  };
3215
+ const joinColPrefix = "__strapi";
3018
3216
  const XtoOne = async (input, ctx) => {
3019
3217
  const { attribute, attributeName, results, populateValue, targetMeta, isCount } = input;
3020
3218
  const { db, qb } = ctx;
@@ -3043,6 +3241,8 @@ const XtoOne = async (input, ctx) => {
3043
3241
  const { name: joinColumnName, referencedColumn: referencedColumnName } = joinTable.joinColumn;
3044
3242
  const alias = qb2.getAlias();
3045
3243
  const joinColAlias = `${alias}.${joinColumnName}`;
3244
+ const joinColRenameAs = `${joinColPrefix}${joinColumnName}`;
3245
+ const joinColSelect = `${joinColAlias} as ${joinColRenameAs}`;
3046
3246
  const referencedValues = ___default.default.uniq(
3047
3247
  results.map((r) => r[referencedColumnName]).filter((value) => !___default.default.isNil(value))
3048
3248
  );
@@ -3061,10 +3261,13 @@ const XtoOne = async (input, ctx) => {
3061
3261
  rootTable: qb2.alias,
3062
3262
  on: joinTable.on
3063
3263
  }).select([joinColAlias, qb2.raw("count(*) AS count")]).where({ [joinColAlias]: referencedValues }).groupBy(joinColAlias).execute({ mapResults: false });
3064
- const map2 = rows2.reduce((map3, row) => {
3065
- map3[row[joinColumnName]] = { count: Number(row.count) };
3066
- return map3;
3067
- }, {});
3264
+ const map2 = rows2.reduce(
3265
+ (map3, row) => {
3266
+ map3[row[joinColumnName]] = { count: Number(row.count) };
3267
+ return map3;
3268
+ },
3269
+ {}
3270
+ );
3068
3271
  results.forEach((result) => {
3069
3272
  result[attributeName] = map2[result[referencedColumnName]] || { count: 0 };
3070
3273
  });
@@ -3084,8 +3287,8 @@ const XtoOne = async (input, ctx) => {
3084
3287
  rootTable: qb2.alias,
3085
3288
  on: joinTable.on,
3086
3289
  orderBy: joinTable.orderBy
3087
- }).addSelect(joinColAlias).where({ [joinColAlias]: referencedValues }).execute({ mapResults: false });
3088
- const map = ___default.default.groupBy(joinColumnName)(rows);
3290
+ }).addSelect(joinColSelect).where({ [joinColAlias]: referencedValues }).execute({ mapResults: false });
3291
+ const map = ___default.default.groupBy(joinColRenameAs)(rows);
3089
3292
  results.forEach((result) => {
3090
3293
  result[attributeName] = fromTargetRow(___default.default.first(map[result[referencedColumnName]]));
3091
3294
  });
@@ -3119,6 +3322,8 @@ const oneToMany = async (input, ctx) => {
3119
3322
  const { name: joinColumnName, referencedColumn: referencedColumnName } = joinTable.joinColumn;
3120
3323
  const alias = qb2.getAlias();
3121
3324
  const joinColAlias = `${alias}.${joinColumnName}`;
3325
+ const joinColRenameAs = `${joinColPrefix}${joinColumnName}`;
3326
+ const joinColSelect = `${joinColAlias} as ${joinColRenameAs}`;
3122
3327
  const referencedValues = ___default.default.uniq(
3123
3328
  results.map((r) => r[referencedColumnName]).filter((value) => !___default.default.isNil(value))
3124
3329
  );
@@ -3136,11 +3341,14 @@ const oneToMany = async (input, ctx) => {
3136
3341
  rootColumn: joinTable.inverseJoinColumn.referencedColumn,
3137
3342
  rootTable: qb2.alias,
3138
3343
  on: joinTable.on
3139
- }).select([joinColAlias, qb2.raw("count(*) AS count")]).where({ [joinColAlias]: referencedValues }).groupBy(joinColAlias).execute({ mapResults: false });
3140
- const map2 = rows2.reduce((map3, row) => {
3141
- map3[row[joinColumnName]] = { count: Number(row.count) };
3142
- return map3;
3143
- }, {});
3344
+ }).select([joinColSelect, qb2.raw("count(*) AS count")]).where({ [joinColAlias]: referencedValues }).groupBy(joinColAlias).execute({ mapResults: false });
3345
+ const map2 = rows2.reduce(
3346
+ (map3, row) => {
3347
+ map3[row[joinColRenameAs]] = { count: Number(row.count) };
3348
+ return map3;
3349
+ },
3350
+ {}
3351
+ );
3144
3352
  results.forEach((result) => {
3145
3353
  result[attributeName] = map2[result[referencedColumnName]] || { count: 0 };
3146
3354
  });
@@ -3160,8 +3368,8 @@ const oneToMany = async (input, ctx) => {
3160
3368
  rootTable: qb2.alias,
3161
3369
  on: joinTable.on,
3162
3370
  orderBy: ___default.default.mapValues((v) => populateValue.ordering || v, joinTable.orderBy)
3163
- }).addSelect(joinColAlias).where({ [joinColAlias]: referencedValues }).execute({ mapResults: false });
3164
- const map = ___default.default.groupBy(joinColumnName)(rows);
3371
+ }).addSelect(joinColSelect).where({ [joinColAlias]: referencedValues }).execute({ mapResults: false });
3372
+ const map = ___default.default.groupBy(joinColRenameAs)(rows);
3165
3373
  results.forEach((r) => {
3166
3374
  r[attributeName] = fromTargetRow(map[r[referencedColumnName]] || []);
3167
3375
  });
@@ -3176,6 +3384,8 @@ const manyToMany = async (input, ctx) => {
3176
3384
  const { name: joinColumnName, referencedColumn: referencedColumnName } = joinTable.joinColumn;
3177
3385
  const alias = populateQb.getAlias();
3178
3386
  const joinColAlias = `${alias}.${joinColumnName}`;
3387
+ const joinColRenameAs = `${joinColPrefix}${joinColumnName}`;
3388
+ const joinColSelect = `${joinColAlias} as ${joinColRenameAs}`;
3179
3389
  const referencedValues = ___default.default.uniq(
3180
3390
  results.map((r) => r[referencedColumnName]).filter((value) => !___default.default.isNil(value))
3181
3391
  );
@@ -3194,10 +3404,13 @@ const manyToMany = async (input, ctx) => {
3194
3404
  rootTable: populateQb.alias,
3195
3405
  on: joinTable.on
3196
3406
  }).select([joinColAlias, populateQb.raw("count(*) AS count")]).where({ [joinColAlias]: referencedValues }).groupBy(joinColAlias).execute({ mapResults: false });
3197
- const map2 = rows2.reduce((map3, row) => {
3198
- map3[row[joinColumnName]] = { count: Number(row.count) };
3199
- return map3;
3200
- }, {});
3407
+ const map2 = rows2.reduce(
3408
+ (map3, row) => {
3409
+ map3[row[joinColumnName]] = { count: Number(row.count) };
3410
+ return map3;
3411
+ },
3412
+ {}
3413
+ );
3201
3414
  results.forEach((result) => {
3202
3415
  result[attributeName] = map2[result[referencedColumnName]] || { count: 0 };
3203
3416
  });
@@ -3217,8 +3430,8 @@ const manyToMany = async (input, ctx) => {
3217
3430
  rootTable: populateQb.alias,
3218
3431
  on: joinTable.on,
3219
3432
  orderBy: ___default.default.mapValues((v) => populateValue.ordering || v, joinTable.orderBy)
3220
- }).addSelect(joinColAlias).where({ [joinColAlias]: referencedValues }).execute({ mapResults: false });
3221
- const map = ___default.default.groupBy(joinColumnName)(rows);
3433
+ }).addSelect(joinColSelect).where({ [joinColAlias]: referencedValues }).execute({ mapResults: false });
3434
+ const map = ___default.default.groupBy(joinColRenameAs)(rows);
3222
3435
  results.forEach((result) => {
3223
3436
  result[attributeName] = fromTargetRow(map[result[referencedColumnName]] || []);
3224
3437
  });
@@ -3544,7 +3757,7 @@ const castValue = (value, attribute) => {
3544
3757
  };
3545
3758
  const processSingleAttributeWhere = (attribute, where, operator = "$eq") => {
3546
3759
  if (!isRecord$1(where)) {
3547
- if (utils$1.isOperatorOfType("cast", operator)) {
3760
+ if (utils.isOperatorOfType("cast", operator)) {
3548
3761
  return castValue(where, attribute);
3549
3762
  }
3550
3763
  return where;
@@ -3552,7 +3765,7 @@ const processSingleAttributeWhere = (attribute, where, operator = "$eq") => {
3552
3765
  const filters = {};
3553
3766
  for (const key of Object.keys(where)) {
3554
3767
  const value = where[key];
3555
- if (!utils$1.isOperatorOfType("where", key)) {
3768
+ if (!utils.isOperatorOfType("where", key)) {
3556
3769
  throw new Error(`Undefined attribute level operator ${key}`);
3557
3770
  }
3558
3771
  filters[key] = processAttributeWhere(attribute, value, key);
@@ -3571,6 +3784,31 @@ const processNested = (where, ctx) => {
3571
3784
  }
3572
3785
  return processWhere(where, ctx);
3573
3786
  };
3787
+ const processRelationWhere = (where, ctx) => {
3788
+ const { qb, alias } = ctx;
3789
+ const idAlias = qb.aliasColumn("id", alias);
3790
+ if (!isRecord$1(where)) {
3791
+ return { [idAlias]: where };
3792
+ }
3793
+ const keys = Object.keys(where);
3794
+ const operatorKeys = keys.filter((key) => utils.isOperator(key));
3795
+ if (operatorKeys.length > 0 && operatorKeys.length !== keys.length) {
3796
+ throw new Error(`Operator and non-operator keys cannot be mixed in a relation where clause`);
3797
+ }
3798
+ if (operatorKeys.length > 1) {
3799
+ throw new Error(
3800
+ `Only one operator key is allowed in a relation where clause, but found: ${operatorKeys}`
3801
+ );
3802
+ }
3803
+ if (operatorKeys.length === 1) {
3804
+ const operator = operatorKeys[0];
3805
+ if (utils.isOperatorOfType("group", operator)) {
3806
+ return processWhere(where, ctx);
3807
+ }
3808
+ return { [idAlias]: { [operator]: processNested(where[operator], ctx) } };
3809
+ }
3810
+ return processWhere(where, ctx);
3811
+ };
3574
3812
  function processWhere(where, ctx) {
3575
3813
  if (!_.isArray(where) && !isRecord$1(where)) {
3576
3814
  throw new Error("Where must be an array or an object");
@@ -3583,7 +3821,10 @@ function processWhere(where, ctx) {
3583
3821
  const filters = {};
3584
3822
  for (const key of Object.keys(where)) {
3585
3823
  const value = where[key];
3586
- if (utils$1.isOperatorOfType("group", key) && Array.isArray(value)) {
3824
+ if (utils.isOperatorOfType("group", key)) {
3825
+ if (!Array.isArray(value)) {
3826
+ throw new Error(`Operator ${key} must be an array`);
3827
+ }
3587
3828
  filters[key] = value.map((sub) => processNested(sub, ctx));
3588
3829
  continue;
3589
3830
  }
@@ -3591,7 +3832,7 @@ function processWhere(where, ctx) {
3591
3832
  filters[key] = processNested(value, ctx);
3592
3833
  continue;
3593
3834
  }
3594
- if (utils$1.isOperatorOfType("where", key)) {
3835
+ if (utils.isOperatorOfType("where", key)) {
3595
3836
  throw new Error(
3596
3837
  `Only $and, $or and $not can only be used as root level operators. Found ${key}.`
3597
3838
  );
@@ -3607,15 +3848,12 @@ function processWhere(where, ctx) {
3607
3848
  attributeName: key,
3608
3849
  attribute
3609
3850
  });
3610
- let nestedWhere = processNested(value, {
3851
+ const nestedWhere = processRelationWhere(value, {
3611
3852
  db,
3612
3853
  qb,
3613
3854
  alias: subAlias,
3614
3855
  uid: attribute.target
3615
3856
  });
3616
- if (!isRecord$1(nestedWhere) || utils$1.isOperatorOfType("where", _.keys(nestedWhere)[0])) {
3617
- nestedWhere = { [qb.aliasColumn("id", subAlias)]: nestedWhere };
3618
- }
3619
3857
  Object.assign(filters, nestedWhere);
3620
3858
  continue;
3621
3859
  }
@@ -3630,7 +3868,7 @@ function processWhere(where, ctx) {
3630
3868
  return filters;
3631
3869
  }
3632
3870
  const applyOperator = (qb, column, operator, value) => {
3633
- if (Array.isArray(value) && !utils$1.isOperatorOfType("array", operator)) {
3871
+ if (Array.isArray(value) && !utils.isOperatorOfType("array", operator)) {
3634
3872
  return qb.where((subQB) => {
3635
3873
  value.forEach(
3636
3874
  (subValue) => subQB.orWhere((innerQB) => {
@@ -3768,8 +4006,8 @@ const applyWhereToColumn = (qb, column, columnWhere) => {
3768
4006
  }
3769
4007
  return qb.where(column, columnWhere);
3770
4008
  }
3771
- const keys2 = Object.keys(columnWhere);
3772
- keys2.forEach((operator) => {
4009
+ const keys = Object.keys(columnWhere);
4010
+ keys.forEach((operator) => {
3773
4011
  const value = columnWhere[operator];
3774
4012
  applyOperator(qb, column, operator, value);
3775
4013
  });
@@ -4708,21 +4946,24 @@ const sortConnectArray = (connectArr, initialArr = [], strictSort = true) => {
4708
4946
  (acc, rel) => ({ ...acc, [rel.id]: true }),
4709
4947
  {}
4710
4948
  );
4711
- const mappedRelations = connectArr.reduce((mapper, relation) => {
4712
- const adjacentRelId = relation.position?.before || relation.position?.after;
4713
- if (!adjacentRelId || !relationInInitialArray[adjacentRelId] && !mapper[adjacentRelId]) {
4714
- needsSorting = true;
4715
- }
4716
- if (mapper[relation.id]) {
4717
- throw new InvalidRelationError(
4718
- `The relation with id ${relation.id} is already connected. You cannot connect the same relation twice.`
4719
- );
4720
- }
4721
- return {
4722
- [relation.id]: { ...relation, computed: false },
4723
- ...mapper
4724
- };
4725
- }, {});
4949
+ const mappedRelations = connectArr.reduce(
4950
+ (mapper, relation) => {
4951
+ const adjacentRelId = relation.position?.before || relation.position?.after;
4952
+ if (!adjacentRelId || !relationInInitialArray[adjacentRelId] && !mapper[adjacentRelId]) {
4953
+ needsSorting = true;
4954
+ }
4955
+ if (mapper[relation.id]) {
4956
+ throw new InvalidRelationError(
4957
+ `The relation with id ${relation.id} is already connected. You cannot connect the same relation twice.`
4958
+ );
4959
+ }
4960
+ return {
4961
+ [relation.id]: { ...relation, computed: false },
4962
+ ...mapper
4963
+ };
4964
+ },
4965
+ {}
4966
+ );
4726
4967
  if (!needsSorting)
4727
4968
  return connectArr;
4728
4969
  const computeRelation = (relation, relationsSeenInBranch) => {
@@ -4830,14 +5071,17 @@ const relationsOrderer = (initArr, idColumn, orderColumn, strict) => {
4830
5071
  * Get a map between the relation id and its order
4831
5072
  */
4832
5073
  getOrderMap() {
4833
- return ___default$1.default(computedRelations).groupBy("order").reduce((acc, relations) => {
4834
- if (relations[0]?.init)
5074
+ return ___default$1.default(computedRelations).groupBy("order").reduce(
5075
+ (acc, relations) => {
5076
+ if (relations[0]?.init)
5077
+ return acc;
5078
+ relations.forEach((relation, idx) => {
5079
+ acc[relation.id] = Math.floor(relation.order) + (idx + 1) / (relations.length + 1);
5080
+ });
4835
5081
  return acc;
4836
- relations.forEach((relation, idx) => {
4837
- acc[relation.id] = Math.floor(relation.order) + (idx + 1) / (relations.length + 1);
4838
- });
4839
- return acc;
4840
- }, {});
5082
+ },
5083
+ {}
5084
+ );
4841
5085
  }
4842
5086
  };
4843
5087
  };
@@ -5591,10 +5835,13 @@ const createEntityManager = (db) => {
5591
5835
  const entry = await this.findOne(uid, {
5592
5836
  select: ["id"],
5593
5837
  where: { id: entity.id },
5594
- populate: fieldsArr.reduce((acc, field) => {
5595
- acc[field] = populate || true;
5596
- return acc;
5597
- }, {})
5838
+ populate: fieldsArr.reduce(
5839
+ (acc, field) => {
5840
+ acc[field] = populate || true;
5841
+ return acc;
5842
+ },
5843
+ {}
5844
+ )
5598
5845
  });
5599
5846
  if (!entry) {
5600
5847
  return null;
@@ -5660,7 +5907,22 @@ const createStorage = (opts) => {
5660
5907
  };
5661
5908
  };
5662
5909
  const wrapTransaction = (db) => (fn) => () => {
5663
- return db.connection.transaction((trx) => Promise.resolve(fn(trx, db)));
5910
+ return db.transaction(({ trx }) => Promise.resolve(fn(trx, db)));
5911
+ };
5912
+ const transformLogMessage = (level, message) => {
5913
+ if (typeof message === "string") {
5914
+ return { level, message };
5915
+ }
5916
+ if (typeof message === "object" && message !== null) {
5917
+ if ("event" in message && "name" in message) {
5918
+ return {
5919
+ level,
5920
+ message: `[internal migration]: ${message.event} ${message?.name}`,
5921
+ timestamp: Date.now()
5922
+ };
5923
+ }
5924
+ }
5925
+ return "";
5664
5926
  };
5665
5927
  const migrationResolver = ({ name, path: path2, context }) => {
5666
5928
  const { db } = context;
@@ -5690,7 +5952,20 @@ const createUserMigrationProvider = (db) => {
5690
5952
  const context = { db };
5691
5953
  const umzugProvider = new umzug.Umzug({
5692
5954
  storage: createStorage({ db, tableName: "strapi_migrations" }),
5693
- logger: console,
5955
+ logger: {
5956
+ info(message) {
5957
+ db.logger.info(transformLogMessage("info", message));
5958
+ },
5959
+ warn(message) {
5960
+ db.logger.warn(transformLogMessage("warn", message));
5961
+ },
5962
+ error(message) {
5963
+ db.logger.error(transformLogMessage("error", message));
5964
+ },
5965
+ debug(message) {
5966
+ db.logger.debug(transformLogMessage("debug", message));
5967
+ }
5968
+ },
5694
5969
  context,
5695
5970
  migrations: {
5696
5971
  glob: ["*.{js,sql}", { cwd: dir }],
@@ -5710,14 +5985,327 @@ const createUserMigrationProvider = (db) => {
5710
5985
  }
5711
5986
  };
5712
5987
  };
5713
- const internalMigrations = [];
5988
+ const QUERIES = {
5989
+ async postgres(knex2, params) {
5990
+ const res = await knex2.raw(
5991
+ `
5992
+ SELECT :tableName:.id as id, string_agg(DISTINCT :inverseJoinColumn:::character varying, ',') as other_ids
5993
+ FROM :tableName:
5994
+ LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:
5995
+ WHERE document_id IS NULL
5996
+ GROUP BY :tableName:.id, :joinColumn:
5997
+ LIMIT 1;
5998
+ `,
5999
+ params
6000
+ );
6001
+ return res.rows;
6002
+ },
6003
+ async mysql(knex2, params) {
6004
+ const [res] = await knex2.raw(
6005
+ `
6006
+ SELECT :tableName:.id as id, group_concat(DISTINCT :inverseJoinColumn:) as other_ids
6007
+ FROM :tableName:
6008
+ LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:
6009
+ WHERE document_id IS NULL
6010
+ GROUP BY :tableName:.id, :joinColumn:
6011
+ LIMIT 1;
6012
+ `,
6013
+ params
6014
+ );
6015
+ return res;
6016
+ },
6017
+ async sqlite(knex2, params) {
6018
+ return knex2.raw(
6019
+ `
6020
+ SELECT :tableName:.id as id, group_concat(DISTINCT :inverseJoinColumn:) as other_ids
6021
+ FROM :tableName:
6022
+ LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:
6023
+ WHERE document_id IS NULL
6024
+ GROUP BY :joinColumn:
6025
+ LIMIT 1;
6026
+ `,
6027
+ params
6028
+ );
6029
+ }
6030
+ };
6031
+ const getNextIdsToCreateDocumentId = async (db, knex2, {
6032
+ joinColumn,
6033
+ inverseJoinColumn,
6034
+ tableName,
6035
+ joinTableName
6036
+ }) => {
6037
+ const res = await QUERIES[db.dialect.client](knex2, {
6038
+ joinColumn,
6039
+ inverseJoinColumn,
6040
+ tableName,
6041
+ joinTableName
6042
+ });
6043
+ if (res.length > 0) {
6044
+ const row = res[0];
6045
+ const otherIds = row.other_ids ? row.other_ids.split(",").map((v) => parseInt(v, 10)) : [];
6046
+ return [row.id, ...otherIds];
6047
+ }
6048
+ return [];
6049
+ };
6050
+ const migrateDocumentIdsWithLocalizations = async (db, knex2, meta) => {
6051
+ const singularName = meta.singularName.toLowerCase();
6052
+ const joinColumn = identifiers.getJoinColumnAttributeIdName(singularName);
6053
+ const inverseJoinColumn = identifiers.getInverseJoinColumnAttributeIdName(singularName);
6054
+ let ids;
6055
+ do {
6056
+ ids = await getNextIdsToCreateDocumentId(db, knex2, {
6057
+ joinColumn,
6058
+ inverseJoinColumn,
6059
+ tableName: meta.tableName,
6060
+ joinTableName: identifiers.getJoinTableName(meta.tableName, `localizations`)
6061
+ });
6062
+ if (ids.length > 0) {
6063
+ await knex2(meta.tableName).update({ document_id: cuid2.createId() }).whereIn("id", ids);
6064
+ }
6065
+ } while (ids.length > 0);
6066
+ };
6067
+ const migrationDocumentIds = async (db, knex2, meta) => {
6068
+ let updatedRows;
6069
+ do {
6070
+ updatedRows = await knex2(meta.tableName).update({ document_id: cuid2.createId() }).whereIn(
6071
+ "id",
6072
+ knex2(meta.tableName).select("id").from(knex2(meta.tableName).select("id").whereNull("document_id").limit(1).as("sub_query"))
6073
+ );
6074
+ } while (updatedRows > 0);
6075
+ };
6076
+ const createDocumentIdColumn = async (knex2, tableName) => {
6077
+ await knex2.schema.alterTable(tableName, (table) => {
6078
+ table.string("document_id");
6079
+ });
6080
+ };
6081
+ const hasLocalizationsJoinTable = async (knex2, tableName) => {
6082
+ const joinTableName = identifiers.getJoinTableName(tableName, "localizations");
6083
+ return knex2.schema.hasTable(joinTableName);
6084
+ };
6085
+ const createdDocumentId = {
6086
+ name: "5.0.0-02-created-document-id",
6087
+ async up(knex2, db) {
6088
+ for (const meta of db.metadata.values()) {
6089
+ const hasTable = await knex2.schema.hasTable(meta.tableName);
6090
+ if (!hasTable) {
6091
+ continue;
6092
+ }
6093
+ if ("documentId" in meta.attributes) {
6094
+ const hasDocumentIdColumn = await knex2.schema.hasColumn(meta.tableName, "document_id");
6095
+ if (hasDocumentIdColumn) {
6096
+ continue;
6097
+ }
6098
+ await createDocumentIdColumn(knex2, meta.tableName);
6099
+ if (await hasLocalizationsJoinTable(knex2, meta.tableName)) {
6100
+ await migrateDocumentIdsWithLocalizations(db, knex2, meta);
6101
+ } else {
6102
+ await migrationDocumentIds(db, knex2, meta);
6103
+ }
6104
+ }
6105
+ }
6106
+ },
6107
+ async down() {
6108
+ throw new Error("not implemented");
6109
+ }
6110
+ };
6111
+ const debug = createDebug__default.default("strapi::database::migration");
6112
+ const renameIdentifiersLongerThanMaxLength = {
6113
+ name: "5.0.0-rename-identifiers-longer-than-max-length",
6114
+ async up(knex2, db) {
6115
+ const md = db.metadata;
6116
+ const diffs = findDiffs(md);
6117
+ for (const indexDiff of diffs.indexes) {
6118
+ await renameIndex(knex2, db, indexDiff);
6119
+ }
6120
+ for (const columnDiff of diffs.columns) {
6121
+ const { full, short } = columnDiff;
6122
+ const tableName = full.tableName;
6123
+ const hasTable = await knex2.schema.hasTable(tableName);
6124
+ if (hasTable) {
6125
+ const hasColumn = await knex2.schema.hasColumn(tableName, full.columnName);
6126
+ if (hasColumn) {
6127
+ await knex2.schema.alterTable(tableName, async (table) => {
6128
+ debug(`renaming column ${full.columnName} to ${short.columnName}`);
6129
+ table.renameColumn(full.columnName, short.columnName);
6130
+ });
6131
+ }
6132
+ }
6133
+ }
6134
+ for (const tableDiff of diffs.tables) {
6135
+ const hasTable = await knex2.schema.hasTable(tableDiff.full.tableName);
6136
+ if (hasTable) {
6137
+ debug(`renaming table ${tableDiff.full.tableName} to ${tableDiff.short.tableName}`);
6138
+ await knex2.schema.renameTable(tableDiff.full.tableName, tableDiff.short.tableName);
6139
+ }
6140
+ }
6141
+ },
6142
+ async down() {
6143
+ throw new Error("not implemented");
6144
+ }
6145
+ };
6146
+ const renameIndex = async (knex2, db, diff) => {
6147
+ const client = db.config.connection.client;
6148
+ const short = diff.short;
6149
+ const full = diff.full;
6150
+ if (full.indexName === short.indexName) {
6151
+ debug(`not renaming index ${full.indexName} because name hasn't changed`);
6152
+ return;
6153
+ }
6154
+ if (short.indexName.includes("_lnk_") || full.indexName.includes("_lnk_") || short.indexName.endsWith("fk") || full.indexName.endsWith("fk")) {
6155
+ return;
6156
+ }
6157
+ debug(`renaming index from ${full.indexName} to ${short.indexName}`);
6158
+ try {
6159
+ await knex2.transaction(async (trx) => {
6160
+ if (client === "mysql" || client === "mariadb") {
6161
+ await knex2.raw(
6162
+ `ALTER TABLE \`${full.tableName}\` RENAME INDEX \`${full.indexName}\` TO \`${short.indexName}\``
6163
+ ).transacting(trx);
6164
+ } else if (client === "pg" || client === "postgres") {
6165
+ await knex2.raw(`ALTER INDEX "${full.indexName}" RENAME TO "${short.indexName}"`).transacting(trx);
6166
+ } else if (client === "sqlite" || client === "better") {
6167
+ } else {
6168
+ debug("No db client name matches, not creating index");
6169
+ }
6170
+ });
6171
+ } catch (err) {
6172
+ debug(`error creating index: ${JSON.stringify(err)}`);
6173
+ }
6174
+ };
6175
+ const findDiffs = (shortMap) => {
6176
+ const diffs = {
6177
+ tables: [],
6178
+ columns: [],
6179
+ indexes: []
6180
+ };
6181
+ const shortArr = Array.from(shortMap.entries());
6182
+ shortArr.forEach(([, shortObj], index2) => {
6183
+ const fullTableName = identifiers.getUnshortenedName(shortObj.tableName);
6184
+ if (!fullTableName) {
6185
+ throw new Error(`Missing full table name for ${shortObj.tableName}`);
6186
+ }
6187
+ if (shortObj.tableName !== fullTableName) {
6188
+ diffs.tables.push({
6189
+ full: {
6190
+ index: index2,
6191
+ key: "tableName",
6192
+ tableName: fullTableName
6193
+ },
6194
+ short: {
6195
+ index: index2,
6196
+ key: "tableName",
6197
+ tableName: shortObj.tableName
6198
+ }
6199
+ });
6200
+ }
6201
+ for (const attrKey in shortObj.attributes) {
6202
+ if (shortObj.attributes[attrKey].type === "relation") {
6203
+ continue;
6204
+ }
6205
+ const attr = shortObj.attributes[attrKey];
6206
+ const shortColumnName = attr.columnName;
6207
+ const longColumnName = identifiers.getUnshortenedName(shortColumnName);
6208
+ if (!shortColumnName || !longColumnName) {
6209
+ throw new Error(`missing column name(s) for attribute ${JSON.stringify(attr, null, 2)}`);
6210
+ }
6211
+ if (shortColumnName && longColumnName && shortColumnName !== longColumnName) {
6212
+ diffs.columns.push({
6213
+ short: {
6214
+ index: index2,
6215
+ tableName: fullTableName,
6216
+ // NOTE: this means that we must rename columns before tables
6217
+ key: `attributes.${attrKey}`,
6218
+ columnName: shortColumnName
6219
+ },
6220
+ full: {
6221
+ index: index2,
6222
+ tableName: fullTableName,
6223
+ key: `attributes.${attrKey}`,
6224
+ columnName: longColumnName
6225
+ }
6226
+ });
6227
+ }
6228
+ }
6229
+ for (const attrKey in shortObj.indexes) {
6230
+ const shortIndexName = shortObj.indexes[attrKey].name;
6231
+ const longIndexName = identifiers.getUnshortenedName(shortIndexName);
6232
+ if (!longIndexName) {
6233
+ throw new Error(`Missing full index name for ${shortIndexName}`);
6234
+ }
6235
+ if (shortIndexName && longIndexName && shortIndexName !== longIndexName) {
6236
+ diffs.indexes.push({
6237
+ short: {
6238
+ index: index2,
6239
+ tableName: fullTableName,
6240
+ // NOTE: this means that we must rename columns before tables
6241
+ key: `indexes.${attrKey}`,
6242
+ indexName: shortIndexName
6243
+ },
6244
+ full: {
6245
+ index: index2,
6246
+ tableName: fullTableName,
6247
+ key: `indexes.${attrKey}`,
6248
+ indexName: longIndexName
6249
+ }
6250
+ });
6251
+ }
6252
+ }
6253
+ });
6254
+ return diffs;
6255
+ };
6256
+ const createLocaleColumn = async (db, tableName) => {
6257
+ await db.schema.alterTable(tableName, (table) => {
6258
+ table.string("locale");
6259
+ });
6260
+ };
6261
+ const createdLocale = {
6262
+ name: "5.0.0-03-created-locale",
6263
+ async up(knex2, db) {
6264
+ for (const meta of db.metadata.values()) {
6265
+ const hasTable = await knex2.schema.hasTable(meta.tableName);
6266
+ if (!hasTable) {
6267
+ continue;
6268
+ }
6269
+ const uid = meta.uid;
6270
+ const model = strapi.getModel(uid);
6271
+ if (!model) {
6272
+ continue;
6273
+ }
6274
+ if (_.isNil(meta.attributes.locale)) {
6275
+ await createLocaleColumn(knex2, meta.tableName);
6276
+ }
6277
+ }
6278
+ },
6279
+ async down() {
6280
+ throw new Error("not implemented");
6281
+ }
6282
+ };
6283
+ const internalMigrations = [
6284
+ renameIdentifiersLongerThanMaxLength,
6285
+ createdDocumentId,
6286
+ createdLocale
6287
+ ];
5714
6288
  const createInternalMigrationProvider = (db) => {
5715
6289
  const context = { db };
6290
+ const migrations = [...internalMigrations];
5716
6291
  const umzugProvider = new umzug.Umzug({
5717
6292
  storage: createStorage({ db, tableName: "strapi_migrations_internal" }),
5718
- logger: console,
6293
+ logger: {
6294
+ info(message) {
6295
+ db.logger.debug(transformLogMessage("info", message));
6296
+ },
6297
+ warn(message) {
6298
+ db.logger.warn(transformLogMessage("warn", message));
6299
+ },
6300
+ error(message) {
6301
+ db.logger.error(transformLogMessage("error", message));
6302
+ },
6303
+ debug(message) {
6304
+ db.logger.debug(transformLogMessage("debug", message));
6305
+ }
6306
+ },
5719
6307
  context,
5720
- migrations: internalMigrations.map((migration) => {
6308
+ migrations: () => migrations.map((migration) => {
5721
6309
  return {
5722
6310
  name: migration.name,
5723
6311
  up: wrapTransaction(context.db)(migration.up),
@@ -5726,6 +6314,9 @@ const createInternalMigrationProvider = (db) => {
5726
6314
  })
5727
6315
  });
5728
6316
  return {
6317
+ async register(migration) {
6318
+ migrations.push(migration);
6319
+ },
5729
6320
  async shouldRun() {
5730
6321
  const pendingMigrations = await umzugProvider.pending();
5731
6322
  return pendingMigrations.length > 0;
@@ -5739,8 +6330,13 @@ const createInternalMigrationProvider = (db) => {
5739
6330
  };
5740
6331
  };
5741
6332
  const createMigrationsProvider = (db) => {
5742
- const providers = [createUserMigrationProvider(db), createInternalMigrationProvider(db)];
6333
+ const userProvider = createUserMigrationProvider(db);
6334
+ const internalProvider = createInternalMigrationProvider(db);
6335
+ const providers = [userProvider, internalProvider];
5743
6336
  return {
6337
+ providers: {
6338
+ internal: internalProvider
6339
+ },
5744
6340
  async shouldRun() {
5745
6341
  const shouldRunResponses = await Promise.all(
5746
6342
  providers.map((provider) => provider.shouldRun())
@@ -5936,8 +6532,14 @@ const validateBidirectionalRelations = async (db) => {
5936
6532
  for (const { relation, invRelation } of invalidLinks) {
5937
6533
  const modelMetadata = db.metadata.get(invRelation.target);
5938
6534
  const invModelMetadata = db.metadata.get(relation.target);
5939
- const joinTableName = getJoinTableName(modelMetadata.tableName, invRelation.inversedBy);
5940
- const inverseJoinTableName = getJoinTableName(invModelMetadata.tableName, relation.inversedBy);
6535
+ const joinTableName = identifiers.getJoinTableName(
6536
+ _.snakeCase(modelMetadata.tableName),
6537
+ _.snakeCase(invRelation.inversedBy)
6538
+ );
6539
+ const inverseJoinTableName = identifiers.getJoinTableName(
6540
+ _.snakeCase(invModelMetadata.tableName),
6541
+ _.snakeCase(relation.inversedBy)
6542
+ );
5941
6543
  const joinTableEmpty = await isLinkTableEmpty(db, joinTableName);
5942
6544
  const inverseJoinTableEmpty = await isLinkTableEmpty(db, inverseJoinTableName);
5943
6545
  if (joinTableEmpty) {
@@ -5972,6 +6574,7 @@ class Database {
5972
6574
  migrations;
5973
6575
  lifecycles;
5974
6576
  entityManager;
6577
+ logger;
5975
6578
  constructor(config) {
5976
6579
  this.config = {
5977
6580
  ...config,
@@ -5983,7 +6586,7 @@ class Database {
5983
6586
  };
5984
6587
  this.dialect = getDialect(this);
5985
6588
  this.dialect.configure();
5986
- this.metadata = createMetadata();
6589
+ this.metadata = createMetadata([]);
5987
6590
  this.connection = createConnection(this.config.connection, {
5988
6591
  pool: { afterCreate: afterCreate(this) }
5989
6592
  });
@@ -5991,6 +6594,7 @@ class Database {
5991
6594
  this.migrations = createMigrationsProvider(this);
5992
6595
  this.lifecycles = createLifecyclesProvider(this);
5993
6596
  this.entityManager = createEntityManager(this);
6597
+ this.logger = config.logger ?? console;
5994
6598
  }
5995
6599
  async init({ models }) {
5996
6600
  this.metadata.loadModels(models);
@@ -6060,9 +6664,7 @@ class Database {
6060
6664
  await this.connection.destroy();
6061
6665
  }
6062
6666
  }
6063
- const utils = { identifiers };
6064
6667
  exports.Database = Database;
6065
6668
  exports.errors = index;
6066
6669
  exports.isKnexQuery = isKnexQuery;
6067
- exports.utils = utils;
6068
6670
  //# sourceMappingURL=index.js.map