@strapi/database 5.0.0-beta.0 → 5.0.0-beta.10

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 (54) 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 +4 -6
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +899 -340
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +897 -338
  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/storage.d.ts.map +1 -1
  34. package/dist/migrations/users.d.ts +2 -2
  35. package/dist/migrations/users.d.ts.map +1 -1
  36. package/dist/query/helpers/join.d.ts.map +1 -1
  37. package/dist/query/helpers/populate/apply.d.ts.map +1 -1
  38. package/dist/query/helpers/where.d.ts +1 -0
  39. package/dist/query/helpers/where.d.ts.map +1 -1
  40. package/dist/schema/index.d.ts +0 -3
  41. package/dist/schema/index.d.ts.map +1 -1
  42. package/dist/transaction-context.d.ts.map +1 -1
  43. package/dist/types/index.d.ts +3 -0
  44. package/dist/types/index.d.ts.map +1 -1
  45. package/dist/utils/identifiers/hash.d.ts +31 -0
  46. package/dist/utils/identifiers/hash.d.ts.map +1 -0
  47. package/dist/utils/identifiers/index.d.ts +107 -47
  48. package/dist/utils/identifiers/index.d.ts.map +1 -1
  49. package/dist/utils/identifiers/types.d.ts +27 -0
  50. package/dist/utils/identifiers/types.d.ts.map +1 -0
  51. package/dist/validations/relations/bidirectional.d.ts.map +1 -1
  52. package/package.json +9 -7
  53. package/dist/utils/identifiers/shortener.d.ts +0 -73
  54. package/dist/utils/identifiers/shortener.d.ts.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,17 +1,18 @@
1
1
  import path from "path";
2
2
  import fse from "fs-extra";
3
3
  import createDebug from "debug";
4
- import _, { isNil, castArray, prop, omit, isInteger, snakeCase, partition, sumBy, toString, toNumber, isString as isString$1, padCharsEnd, isArray, keys, isPlainObject, isFinite, groupBy, pipe, mapValues, map, isEmpty, maxBy, pick, has, uniqBy, isNull, differenceWith, isEqual, compact, difference, isObject, isNumber as isNumber$1, isUndefined, uniqWith } from "lodash/fp";
4
+ import _, { isNil, castArray, prop, omit, isInteger, snakeCase, partition, sumBy, cloneDeep, toString, toNumber, isString as isString$1, padCharsEnd, isArray, isPlainObject, isFinite, groupBy, pipe, mapValues, map, isEmpty, maxBy, pick, has, uniqBy, isNull, differenceWith, isEqual, compact, difference, isObject, isNumber as isNumber$1, isUndefined, uniqWith } from "lodash/fp";
5
5
  import crypto from "crypto";
6
6
  import crypto$1 from "node:crypto";
7
7
  import * as dateFns from "date-fns";
8
- import { isOperatorOfType } from "@strapi/utils";
8
+ import { isOperatorOfType, isOperator } from "@strapi/utils";
9
9
  import KnexBuilder from "knex/lib/query/querybuilder";
10
10
  import KnexRaw from "knex/lib/raw";
11
11
  import { Readable } from "stream";
12
12
  import { AsyncLocalStorage } from "node:async_hooks";
13
13
  import _$1 from "lodash";
14
14
  import { Umzug } from "umzug";
15
+ import { createId } from "@paralleldrive/cuid2";
15
16
  import { strict } from "assert";
16
17
  import knex from "knex";
17
18
  class Dialect {
@@ -975,7 +976,7 @@ const getDialect = (db) => {
975
976
  const dialect = new constructor(db, dialectName);
976
977
  return dialect;
977
978
  };
978
- const debug$1 = createDebug("strapi::database");
979
+ const debug$2 = createDebug("strapi::database");
979
980
  const createSchemaBuilder = (db) => {
980
981
  const helpers2 = createHelpers(db);
981
982
  return {
@@ -1001,12 +1002,12 @@ const createSchemaBuilder = (db) => {
1001
1002
  */
1002
1003
  async createTables(tables, trx) {
1003
1004
  for (const table of tables) {
1004
- debug$1(`Creating table: ${table.name}`);
1005
+ debug$2(`Creating table: ${table.name}`);
1005
1006
  const schemaBuilder = this.getSchemaBuilder(trx);
1006
1007
  await helpers2.createTable(schemaBuilder, table);
1007
1008
  }
1008
1009
  for (const table of tables) {
1009
- debug$1(`Creating table foreign keys: ${table.name}`);
1010
+ debug$2(`Creating table foreign keys: ${table.name}`);
1010
1011
  const schemaBuilder = this.getSchemaBuilder(trx);
1011
1012
  await helpers2.createTableForeignKeys(schemaBuilder, table);
1012
1013
  }
@@ -1037,18 +1038,18 @@ const createSchemaBuilder = (db) => {
1037
1038
  await this.createTables(schemaDiff.tables.added, trx);
1038
1039
  if (forceMigration) {
1039
1040
  for (const table of schemaDiff.tables.removed) {
1040
- debug$1(`Removing table foreign keys: ${table.name}`);
1041
+ debug$2(`Removing table foreign keys: ${table.name}`);
1041
1042
  const schemaBuilder = this.getSchemaBuilder(trx);
1042
1043
  await helpers2.dropTableForeignKeys(schemaBuilder, table);
1043
1044
  }
1044
1045
  for (const table of schemaDiff.tables.removed) {
1045
- debug$1(`Removing table: ${table.name}`);
1046
+ debug$2(`Removing table: ${table.name}`);
1046
1047
  const schemaBuilder = this.getSchemaBuilder(trx);
1047
1048
  await helpers2.dropTable(schemaBuilder, table);
1048
1049
  }
1049
1050
  }
1050
1051
  for (const table of schemaDiff.tables.updated) {
1051
- debug$1(`Updating table: ${table.name}`);
1052
+ debug$2(`Updating table: ${table.name}`);
1052
1053
  const schemaBuilder = this.getSchemaBuilder(trx);
1053
1054
  await helpers2.alterTable(schemaBuilder, table);
1054
1055
  }
@@ -1143,28 +1144,37 @@ const createHelpers = (db) => {
1143
1144
  };
1144
1145
  const alterTable = async (schemaBuilder, table) => {
1145
1146
  await schemaBuilder.alterTable(table.name, (tableBuilder) => {
1146
- for (const removedIndex of table.indexes.removed) {
1147
- debug$1(`Dropping index ${removedIndex.name}`);
1148
- dropIndex(tableBuilder, removedIndex);
1149
- }
1150
- for (const updateddIndex of table.indexes.updated) {
1151
- debug$1(`Dropping updated index ${updateddIndex.name}`);
1152
- dropIndex(tableBuilder, updateddIndex.object);
1153
- }
1154
1147
  for (const removedForeignKey of table.foreignKeys.removed) {
1155
- debug$1(`Dropping foreign key ${removedForeignKey.name}`);
1148
+ debug$2(`Dropping foreign key ${removedForeignKey.name} on ${table.name}`);
1156
1149
  dropForeignKey(tableBuilder, removedForeignKey);
1157
1150
  }
1158
1151
  for (const updatedForeignKey of table.foreignKeys.updated) {
1159
- debug$1(`Dropping updated foreign key ${updatedForeignKey.name}`);
1152
+ debug$2(`Dropping updated foreign key ${updatedForeignKey.name} on ${table.name}`);
1160
1153
  dropForeignKey(tableBuilder, updatedForeignKey.object);
1161
1154
  }
1162
1155
  for (const removedColumn of table.columns.removed) {
1163
- debug$1(`Dropping column ${removedColumn.name}`);
1156
+ debug$2(`Dropping column ${removedColumn.name} on ${table.name}`);
1164
1157
  dropColumn(tableBuilder, removedColumn);
1165
1158
  }
1159
+ const isMySQL = db.config.connection.client === "mysql";
1160
+ const ignoreForeignKeyNames = isMySQL ? [
1161
+ ...table.foreignKeys.removed.map((fk) => fk.name),
1162
+ ...table.foreignKeys.updated.map((fk) => fk.name)
1163
+ ] : [];
1164
+ for (const removedIndex of table.indexes.removed) {
1165
+ if (!ignoreForeignKeyNames.includes(removedIndex.name)) {
1166
+ debug$2(`Dropping index ${removedIndex.name} on ${table.name}`);
1167
+ dropIndex(tableBuilder, removedIndex);
1168
+ }
1169
+ }
1170
+ for (const updatedIndex of table.indexes.updated) {
1171
+ if (!ignoreForeignKeyNames.includes(updatedIndex.name)) {
1172
+ debug$2(`Dropping updated index ${updatedIndex.name} on ${table.name}`);
1173
+ dropIndex(tableBuilder, updatedIndex.object);
1174
+ }
1175
+ }
1166
1176
  for (const updatedColumn of table.columns.updated) {
1167
- debug$1(`Updating column ${updatedColumn.name}`);
1177
+ debug$2(`Updating column ${updatedColumn.name} on ${table.name}`);
1168
1178
  const { object } = updatedColumn;
1169
1179
  if (object.type === "increments") {
1170
1180
  createColumn2(tableBuilder, { ...object, type: "integer" }).alter();
@@ -1173,15 +1183,15 @@ const createHelpers = (db) => {
1173
1183
  }
1174
1184
  }
1175
1185
  for (const updatedForeignKey of table.foreignKeys.updated) {
1176
- debug$1(`Recreating updated foreign key ${updatedForeignKey.name}`);
1186
+ debug$2(`Recreating updated foreign key ${updatedForeignKey.name} on ${table.name}`);
1177
1187
  createForeignKey(tableBuilder, updatedForeignKey.object);
1178
1188
  }
1179
1189
  for (const updatedIndex of table.indexes.updated) {
1180
- debug$1(`Recreating updated index ${updatedIndex.name}`);
1190
+ debug$2(`Recreating updated index ${updatedIndex.name} on ${table.name}`);
1181
1191
  createIndex(tableBuilder, updatedIndex.object);
1182
1192
  }
1183
1193
  for (const addedColumn of table.columns.added) {
1184
- debug$1(`Creating column ${addedColumn.name}`);
1194
+ debug$2(`Creating column ${addedColumn.name} on ${table.name}`);
1185
1195
  if (addedColumn.type === "increments" && !db.dialect.canAddIncrements()) {
1186
1196
  tableBuilder.integer(addedColumn.name).unsigned();
1187
1197
  tableBuilder.primary([addedColumn.name]);
@@ -1190,11 +1200,11 @@ const createHelpers = (db) => {
1190
1200
  }
1191
1201
  }
1192
1202
  for (const addedForeignKey of table.foreignKeys.added) {
1193
- debug$1(`Creating foreign keys ${addedForeignKey.name}`);
1203
+ debug$2(`Creating foreign keys ${addedForeignKey.name} on ${table.name}`);
1194
1204
  createForeignKey(tableBuilder, addedForeignKey);
1195
1205
  }
1196
1206
  for (const addedIndex of table.indexes.added) {
1197
- debug$1(`Creating index ${addedIndex.name}`);
1207
+ debug$2(`Creating index ${addedIndex.name} on ${table.name}`);
1198
1208
  createIndex(tableBuilder, addedIndex);
1199
1209
  }
1200
1210
  });
@@ -1613,11 +1623,6 @@ const isScalar = (type) => SCALAR_TYPES.includes(type);
1613
1623
  const isRelation = (type) => type === "relation";
1614
1624
  const isScalarAttribute = (attribute) => isScalar(attribute.type);
1615
1625
  const isRelationalAttribute = (attribute) => isRelation(attribute.type);
1616
- const MAX_DB_IDENTIFIER_LENGTH = 0;
1617
- const HASH_LENGTH = 5;
1618
- const HASH_SEPARATOR = "";
1619
- const IDENTIFIER_SEPARATOR = "_";
1620
- const MIN_TOKEN_LENGTH = 3;
1621
1626
  function createHash(data, len) {
1622
1627
  if (!isInteger(len) || len <= 0) {
1623
1628
  throw new Error(`createHash length must be a positive integer, received ${len}`);
@@ -1625,201 +1630,334 @@ function createHash(data, len) {
1625
1630
  const hash = crypto$1.createHash("shake256", { outputLength: Math.ceil(len / 2) }).update(data);
1626
1631
  return hash.digest("hex").substring(0, len);
1627
1632
  }
1628
- function getShortenedName(name, len) {
1629
- if (!isInteger(len) || len <= 0) {
1630
- throw new Error(`tokenWithHash length must be a positive integer, received ${len}`);
1631
- }
1632
- if (name.length <= len) {
1633
- return name;
1634
- }
1635
- if (len < MIN_TOKEN_LENGTH + HASH_LENGTH) {
1636
- throw new Error(
1637
- `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}`
1638
- );
1639
- }
1640
- const availableLength = len - HASH_LENGTH - HASH_SEPARATOR.length;
1641
- if (availableLength < MIN_TOKEN_LENGTH) {
1642
- throw new Error(
1643
- `length for part of identifier minimum is less than min token length (${MIN_TOKEN_LENGTH}), received ${len} for token ${name}`
1644
- );
1633
+ const IDENTIFIER_MAX_LENGTH = 55;
1634
+ class Identifiers {
1635
+ ID_COLUMN = "id";
1636
+ ORDER_COLUMN = "order";
1637
+ FIELD_COLUMN = "field";
1638
+ HASH_LENGTH = 5;
1639
+ HASH_SEPARATOR = "";
1640
+ // no separator is needed, we will just attach hash directly to shortened name
1641
+ IDENTIFIER_SEPARATOR = "_";
1642
+ MIN_TOKEN_LENGTH = 3;
1643
+ // the min characters required at the beginning of a name part
1644
+ // Fixed compression map for suffixes and prefixes
1645
+ #replacementMap = {
1646
+ links: "lnk",
1647
+ order_inv_fk: "oifk",
1648
+ order: "ord",
1649
+ morphs: "mph",
1650
+ index: "idx",
1651
+ inv_fk: "ifk",
1652
+ order_fk: "ofk",
1653
+ id_column_index: "idix",
1654
+ order_index: "oidx",
1655
+ unique: "uq",
1656
+ primary: "pk"
1657
+ };
1658
+ #options;
1659
+ constructor(options) {
1660
+ this.#options = options;
1645
1661
  }
1646
- return `${name.substring(0, availableLength)}${HASH_SEPARATOR}${createHash(name, HASH_LENGTH)}`;
1647
- }
1648
- function getNameFromTokens(nameTokens, maxLength = MAX_DB_IDENTIFIER_LENGTH) {
1649
- if (!isInteger(maxLength) || maxLength < 0) {
1650
- throw new Error("maxLength must be a positive integer or 0 (for unlimited length)");
1662
+ get replacementMap() {
1663
+ return this.#replacementMap;
1651
1664
  }
1652
- const fullLengthName = nameTokens.map((token) => snakeCase(token.name)).join(IDENTIFIER_SEPARATOR);
1653
- if (fullLengthName.length <= maxLength || maxLength === 0) {
1654
- return fullLengthName;
1665
+ get options() {
1666
+ return this.#options;
1655
1667
  }
1656
- const [compressible, incompressible] = partition(
1657
- (token) => token.compressible,
1658
- nameTokens
1659
- );
1660
- const totalIncompressibleLength = sumBy((token) => token.name.length)(incompressible);
1661
- const totalSeparatorsLength = nameTokens.length * IDENTIFIER_SEPARATOR.length - 1;
1662
- const available = maxLength - totalIncompressibleLength - totalSeparatorsLength;
1663
- const availablePerToken = Math.floor(available / compressible.length);
1664
- if (totalIncompressibleLength + totalSeparatorsLength > maxLength || availablePerToken < MIN_TOKEN_LENGTH) {
1665
- throw new Error("Maximum length is too small to accommodate all tokens");
1666
- }
1667
- let surplus = available % compressible.length;
1668
- const minHashedLength = HASH_LENGTH + HASH_SEPARATOR.length + MIN_TOKEN_LENGTH;
1669
- const totalLength = nameTokens.reduce((total, token) => {
1670
- if (token.compressible) {
1671
- if (token.name.length < availablePerToken) {
1672
- return total + token.name.length;
1673
- }
1674
- return total + minHashedLength;
1675
- }
1676
- return total + token.name.length;
1677
- }, nameTokens.length * IDENTIFIER_SEPARATOR.length - 1);
1678
- if (maxLength < totalLength) {
1679
- throw new Error("Maximum length is too small to accommodate all tokens");
1680
- }
1681
- let deficits = [];
1682
- compressible.forEach((token) => {
1683
- const actualLength = token.name.length;
1684
- if (actualLength < availablePerToken) {
1685
- surplus += availablePerToken - actualLength;
1686
- token.allocatedLength = actualLength;
1687
- } else {
1688
- token.allocatedLength = availablePerToken;
1689
- deficits.push(token);
1668
+ mapshortNames = (name) => {
1669
+ if (name in this.replacementMap) {
1670
+ return this.replacementMap[name];
1690
1671
  }
1691
- });
1692
- function filterAndIncreaseLength(token) {
1693
- if (token.allocatedLength < token.name.length && surplus > 0) {
1694
- token.allocatedLength += 1;
1695
- surplus -= 1;
1696
- return token.allocatedLength < token.name.length;
1672
+ return void 0;
1673
+ };
1674
+ // Generic name handler that must be used by all helper functions
1675
+ /**
1676
+ * TODO: we should be requiring snake_case inputs for all names here, but we
1677
+ * aren't and it will require some refactoring to make it work. Currently if
1678
+ * we get names 'myModel' and 'my_model' they would be converted to the same
1679
+ * final string my_model which generally works but is not entirely safe
1680
+ * */
1681
+ getName = (names, options) => {
1682
+ const tokens = _.castArray(names).map((name) => {
1683
+ return {
1684
+ name,
1685
+ compressible: true
1686
+ };
1687
+ });
1688
+ if (options?.suffix) {
1689
+ tokens.push({
1690
+ name: options.suffix,
1691
+ compressible: false,
1692
+ shortName: this.mapshortNames(options.suffix)
1693
+ });
1697
1694
  }
1698
- return false;
1699
- }
1700
- let previousSurplus = surplus + 1;
1701
- while (surplus > 0 && deficits.length > 0) {
1702
- deficits = deficits.filter((token) => filterAndIncreaseLength(token));
1703
- if (surplus === previousSurplus) {
1704
- break;
1695
+ if (options?.prefix) {
1696
+ tokens.unshift({
1697
+ name: options.prefix,
1698
+ compressible: false,
1699
+ shortName: this.mapshortNames(options.prefix)
1700
+ });
1705
1701
  }
1706
- previousSurplus = surplus;
1707
- }
1708
- const shortenedName = nameTokens.map((token) => {
1709
- if (token.compressible && "allocatedLength" in token && token.allocatedLength !== void 0) {
1710
- return getShortenedName(token.name, token.allocatedLength);
1702
+ return this.getNameFromTokens(tokens);
1703
+ };
1704
+ /**
1705
+ * TABLES
1706
+ */
1707
+ getTableName = (name, options) => {
1708
+ return this.getName(name, options);
1709
+ };
1710
+ getJoinTableName = (collectionName, attributeName, options) => {
1711
+ return this.getName([collectionName, attributeName], {
1712
+ suffix: "links",
1713
+ ...options
1714
+ });
1715
+ };
1716
+ getMorphTableName = (collectionName, attributeName, options) => {
1717
+ return this.getName([snakeCase(collectionName), snakeCase(attributeName)], {
1718
+ suffix: "morphs",
1719
+ ...options
1720
+ });
1721
+ };
1722
+ /**
1723
+ * COLUMNS
1724
+ */
1725
+ getColumnName = (attributeName, options) => {
1726
+ return this.getName(attributeName, options);
1727
+ };
1728
+ getJoinColumnAttributeIdName = (attributeName, options) => {
1729
+ return this.getName(attributeName, { suffix: "id", ...options });
1730
+ };
1731
+ getInverseJoinColumnAttributeIdName = (attributeName, options) => {
1732
+ return this.getName(snakeCase(attributeName), { suffix: "id", prefix: "inv", ...options });
1733
+ };
1734
+ getOrderColumnName = (singularName, options) => {
1735
+ return this.getName(singularName, { suffix: "order", ...options });
1736
+ };
1737
+ getInverseOrderColumnName = (singularName, options) => {
1738
+ return this.getName(singularName, {
1739
+ suffix: "order",
1740
+ prefix: "inv",
1741
+ ...options
1742
+ });
1743
+ };
1744
+ /**
1745
+ * Morph Join Tables
1746
+ */
1747
+ getMorphColumnJoinTableIdName = (singularName, options) => {
1748
+ return this.getName(snakeCase(singularName), { suffix: "id", ...options });
1749
+ };
1750
+ getMorphColumnAttributeIdName = (attributeName, options) => {
1751
+ return this.getName(snakeCase(attributeName), { suffix: "id", ...options });
1752
+ };
1753
+ getMorphColumnTypeName = (attributeName, options) => {
1754
+ return this.getName(snakeCase(attributeName), { suffix: "type", ...options });
1755
+ };
1756
+ /**
1757
+ * INDEXES
1758
+ * Note that these methods are generally used to reference full table names + attribute(s), which
1759
+ * may already be shortened strings rather than individual parts.
1760
+ * That is fine and expected to compress the previously incompressible parts of those strings,
1761
+ * because in these cases the relevant information is the table name and we can't really do
1762
+ * any better; shortening the individual parts again might make it even more confusing.
1763
+ *
1764
+ * So for example, the fk for the table `mytable_myattr4567d_localizations` will become
1765
+ * mytable_myattr4567d_loc63bf2_fk
1766
+ */
1767
+ // base index types
1768
+ getIndexName = (names, options) => {
1769
+ return this.getName(names, { suffix: "index", ...options });
1770
+ };
1771
+ getFkIndexName = (names, options) => {
1772
+ return this.getName(names, { suffix: "fk", ...options });
1773
+ };
1774
+ getUniqueIndexName = (names, options) => {
1775
+ return this.getName(names, { suffix: "unique", ...options });
1776
+ };
1777
+ getPrimaryIndexName = (names, options) => {
1778
+ return this.getName(names, { suffix: "primary", ...options });
1779
+ };
1780
+ // custom index types
1781
+ getInverseFkIndexName = (names, options) => {
1782
+ return this.getName(names, { suffix: "inv_fk", ...options });
1783
+ };
1784
+ getOrderFkIndexName = (names, options) => {
1785
+ return this.getName(names, { suffix: "order_fk", ...options });
1786
+ };
1787
+ getOrderInverseFkIndexName = (names, options) => {
1788
+ return this.getName(names, { suffix: "order_inv_fk", ...options });
1789
+ };
1790
+ getIdColumnIndexName = (names, options) => {
1791
+ return this.getName(names, { suffix: "id_column_index", ...options });
1792
+ };
1793
+ getOrderIndexName = (names, options) => {
1794
+ return this.getName(names, { suffix: "order_index", ...options });
1795
+ };
1796
+ /**
1797
+ * Generates a string with a max length, appending a hash at the end if necessary to keep it unique
1798
+ *
1799
+ * @example
1800
+ * // if we have strings such as "longstring1" and "longstring2" with a max length of 9,
1801
+ * // we don't want to end up with "longstrin" and "longstrin"
1802
+ * // we want something such as "longs0b23" and "longs953f"
1803
+ * const token1 = generateToken("longstring1", 9); // "longs0b23"
1804
+ * const token2 = generateToken("longstring2", 9); // "longs953f"
1805
+ *
1806
+ * @param name - The base name
1807
+ * @param len - The desired length of the token.
1808
+ * @returns The generated token with hash.
1809
+ * @throws Error if the length is not a positive integer, or if the length is too short for the token.
1810
+ * @internal
1811
+ */
1812
+ getShortenedName = (name, len) => {
1813
+ if (!isInteger(len) || len <= 0) {
1814
+ throw new Error(`tokenWithHash length must be a positive integer, received ${len}`);
1711
1815
  }
1712
- return token.name;
1713
- }).join(IDENTIFIER_SEPARATOR);
1714
- if (shortenedName.length > maxLength) {
1715
- throw new Error(
1716
- `name shortening failed to generate a name of the correct maxLength; name ${shortenedName}`
1816
+ if (name.length <= len) {
1817
+ return name;
1818
+ }
1819
+ if (len < this.MIN_TOKEN_LENGTH + this.HASH_LENGTH) {
1820
+ throw new Error(
1821
+ `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}`
1822
+ );
1823
+ }
1824
+ const availableLength = len - this.HASH_LENGTH - this.HASH_SEPARATOR.length;
1825
+ if (availableLength < this.MIN_TOKEN_LENGTH) {
1826
+ throw new Error(
1827
+ `length for part of identifier minimum is less than min token length (${this.MIN_TOKEN_LENGTH}), received ${len} for token ${name}`
1828
+ );
1829
+ }
1830
+ return `${name.substring(0, availableLength)}${this.HASH_SEPARATOR}${createHash(
1831
+ name,
1832
+ this.HASH_LENGTH
1833
+ )}`;
1834
+ };
1835
+ /**
1836
+ * Constructs a name from an array of name tokens within a specified maximum length. It ensures the final name does not exceed
1837
+ * this limit by selectively compressing tokens marked as compressible. If the name exceeds the maximum length and cannot be
1838
+ * compressed sufficiently, an error is thrown. This function supports dynamic adjustment of token lengths to fit within the
1839
+ * maxLength constraint (that is, it will always make use of all available space), while also ensuring the preservation of
1840
+ * incompressible tokens.
1841
+ * @internal
1842
+ */
1843
+ getNameFromTokens = (nameTokens) => {
1844
+ const { maxLength } = this.options;
1845
+ if (!isInteger(maxLength) || maxLength < 0) {
1846
+ throw new Error("maxLength must be a positive integer or 0 (for unlimited length)");
1847
+ }
1848
+ const unshortenedName = nameTokens.map((token) => {
1849
+ return token.name;
1850
+ }).join(this.IDENTIFIER_SEPARATOR);
1851
+ if (maxLength === 0) {
1852
+ this.setUnshortenedName(unshortenedName, unshortenedName);
1853
+ return unshortenedName;
1854
+ }
1855
+ const fullLengthName = nameTokens.map((token) => {
1856
+ if (token.compressible) {
1857
+ return token.name;
1858
+ }
1859
+ return token.shortName ?? token.name;
1860
+ }).join(this.IDENTIFIER_SEPARATOR);
1861
+ if (fullLengthName.length <= maxLength) {
1862
+ this.setUnshortenedName(fullLengthName, unshortenedName);
1863
+ return fullLengthName;
1864
+ }
1865
+ const [compressible, incompressible] = partition(
1866
+ (token) => token.compressible,
1867
+ nameTokens
1717
1868
  );
1718
- }
1719
- return shortenedName;
1869
+ const totalIncompressibleLength = sumBy(
1870
+ (token) => token.compressible === false && token.shortName !== void 0 ? token.shortName.length : token.name.length
1871
+ )(incompressible);
1872
+ const totalSeparatorsLength = nameTokens.length * this.IDENTIFIER_SEPARATOR.length - 1;
1873
+ const available = maxLength - totalIncompressibleLength - totalSeparatorsLength;
1874
+ const availablePerToken = Math.floor(available / compressible.length);
1875
+ if (totalIncompressibleLength + totalSeparatorsLength > maxLength || availablePerToken < this.MIN_TOKEN_LENGTH) {
1876
+ throw new Error("Maximum length is too small to accommodate all tokens");
1877
+ }
1878
+ let surplus = available % compressible.length;
1879
+ const minHashedLength = this.HASH_LENGTH + this.HASH_SEPARATOR.length + this.MIN_TOKEN_LENGTH;
1880
+ const totalLength = nameTokens.reduce(
1881
+ (total, token) => {
1882
+ if (token.compressible) {
1883
+ if (token.name.length < availablePerToken) {
1884
+ return total + token.name.length;
1885
+ }
1886
+ return total + minHashedLength;
1887
+ }
1888
+ const tokenName = token.shortName ?? token.name;
1889
+ return total + tokenName.length;
1890
+ },
1891
+ nameTokens.length * this.IDENTIFIER_SEPARATOR.length - 1
1892
+ );
1893
+ if (maxLength < totalLength) {
1894
+ throw new Error("Maximum length is too small to accommodate all tokens");
1895
+ }
1896
+ let deficits = [];
1897
+ compressible.forEach((token) => {
1898
+ const actualLength = token.name.length;
1899
+ if (actualLength < availablePerToken) {
1900
+ surplus += availablePerToken - actualLength;
1901
+ token.allocatedLength = actualLength;
1902
+ } else {
1903
+ token.allocatedLength = availablePerToken;
1904
+ deficits.push(token);
1905
+ }
1906
+ });
1907
+ function filterAndIncreaseLength(token) {
1908
+ if (token.allocatedLength < token.name.length && surplus > 0) {
1909
+ token.allocatedLength += 1;
1910
+ surplus -= 1;
1911
+ return token.allocatedLength < token.name.length;
1912
+ }
1913
+ return false;
1914
+ }
1915
+ let previousSurplus = surplus + 1;
1916
+ while (surplus > 0 && deficits.length > 0) {
1917
+ deficits = deficits.filter((token) => filterAndIncreaseLength(token));
1918
+ if (surplus === previousSurplus) {
1919
+ break;
1920
+ }
1921
+ previousSurplus = surplus;
1922
+ }
1923
+ const shortenedName = nameTokens.map((token) => {
1924
+ if (token.compressible && "allocatedLength" in token && token.allocatedLength !== void 0) {
1925
+ return this.getShortenedName(token.name, token.allocatedLength);
1926
+ }
1927
+ if (token.compressible === false && token.shortName) {
1928
+ return token.shortName;
1929
+ }
1930
+ return token.name;
1931
+ }).join(this.IDENTIFIER_SEPARATOR);
1932
+ if (shortenedName.length > maxLength) {
1933
+ throw new Error(
1934
+ `name shortening failed to generate a name of the correct maxLength; name ${shortenedName}`
1935
+ );
1936
+ }
1937
+ this.setUnshortenedName(shortenedName, unshortenedName);
1938
+ return shortenedName;
1939
+ };
1940
+ // We need to be able to find the full-length name for any shortened name, primarily for migration purposes
1941
+ // Therefore we store every name that passes through so we can retrieve the original later
1942
+ nameMap = /* @__PURE__ */ new Map();
1943
+ getUnshortenedName = (shortName) => {
1944
+ return this.nameMap.get(this.serializeKey(shortName)) ?? shortName;
1945
+ };
1946
+ setUnshortenedName = (shortName, fullName) => {
1947
+ if (this.nameMap.get(this.serializeKey(shortName)) && shortName === fullName) {
1948
+ return;
1949
+ }
1950
+ this.nameMap.set(this.serializeKey(shortName), fullName);
1951
+ };
1952
+ serializeKey = (shortName) => {
1953
+ return `${shortName}.${this.options.maxLength}`;
1954
+ };
1720
1955
  }
1721
- const ID_COLUMN = "id";
1722
- const ORDER_COLUMN = "order";
1723
- const FIELD_COLUMN = "field";
1724
- const getName = (names, options) => {
1725
- const tokens = _.castArray(names).map((name) => {
1726
- return {
1727
- name,
1728
- compressible: true
1729
- };
1730
- });
1731
- if (options?.suffix) {
1732
- tokens.push({ name: options.suffix, compressible: false });
1733
- }
1734
- if (options?.prefix) {
1735
- tokens.unshift({ name: options.prefix, compressible: false });
1736
- }
1737
- const maxLength = options?.maxLength ?? MAX_DB_IDENTIFIER_LENGTH;
1738
- return getNameFromTokens(tokens, maxLength);
1739
- };
1740
- const getTableName = (name, options) => {
1741
- return getName(name, options);
1742
- };
1743
- const getJoinTableName = (collectionName, attributeName, options) => {
1744
- return getName([collectionName, attributeName], { suffix: "links", ...options });
1745
- };
1746
- const getMorphTableName = (collectionName, attributeName, options) => {
1747
- return getName([collectionName, attributeName], { suffix: "morphs", ...options });
1748
- };
1749
- const getColumnName = (attributeName, options) => {
1750
- return getName(attributeName, options);
1751
- };
1752
- const getJoinColumnAttributeIdName = (attributeName, options) => {
1753
- return getName(attributeName, { suffix: "id", ...options });
1754
- };
1755
- const getInverseJoinColumnAttributeIdName = (attributeName, options) => {
1756
- return getName(attributeName, { suffix: "id", prefix: "inv", ...options });
1757
- };
1758
- const getOrderColumnName = (singularName, options) => {
1759
- return getName(singularName, { suffix: "order", ...options });
1760
- };
1761
- const getInverseOrderColumnName = (singularName, options) => {
1762
- return getName(singularName, { suffix: "order", prefix: "inv", ...options });
1763
- };
1764
- const getMorphColumnJoinTableIdName = (singularName, options) => {
1765
- return getName(singularName, { suffix: "id", ...options });
1766
- };
1767
- const getMorphColumnAttributeIdName = (attributeName, options) => {
1768
- return getName(attributeName, { suffix: "id", ...options });
1769
- };
1770
- const getMorphColumnTypeName = (attributeName, options) => {
1771
- return getName(attributeName, { suffix: "type", ...options });
1772
- };
1773
- const getIndexName = (names, options) => {
1774
- return getName(names, { suffix: "index", ...options });
1775
- };
1776
- const getFkIndexName = (names, options) => {
1777
- return getName(names, { suffix: "fk", ...options });
1778
- };
1779
- const getInverseFkIndexName = (names, options) => {
1780
- return getName(names, { suffix: "inv_fk", ...options });
1781
- };
1782
- const getOrderFkIndexName = (names, options) => {
1783
- return getName(names, { suffix: "order_fk", ...options });
1784
- };
1785
- const getOrderInverseFkIndexName = (names, options) => {
1786
- return getName(names, { suffix: "order_inv_fk", ...options });
1787
- };
1788
- const getUniqueIndexName = (names, options) => {
1789
- return getName(names, { suffix: "unique", ...options });
1790
- };
1791
- const getPrimaryIndexName = (names) => {
1792
- return getName(names, { suffix: "primary" });
1793
- };
1794
- const identifiers = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1795
- __proto__: null,
1796
- FIELD_COLUMN,
1797
- ID_COLUMN,
1798
- ORDER_COLUMN,
1799
- getColumnName,
1800
- getFkIndexName,
1801
- getIndexName,
1802
- getInverseFkIndexName,
1803
- getInverseJoinColumnAttributeIdName,
1804
- getInverseOrderColumnName,
1805
- getJoinColumnAttributeIdName,
1806
- getJoinTableName,
1807
- getMorphColumnAttributeIdName,
1808
- getMorphColumnJoinTableIdName,
1809
- getMorphColumnTypeName,
1810
- getMorphTableName,
1811
- getName,
1812
- getOrderColumnName,
1813
- getOrderFkIndexName,
1814
- getOrderInverseFkIndexName,
1815
- getPrimaryIndexName,
1816
- getTableName,
1817
- getUniqueIndexName
1818
- }, Symbol.toStringTag, { value: "Module" }));
1956
+ const identifiers = new Identifiers({ maxLength: IDENTIFIER_MAX_LENGTH });
1819
1957
  const createColumn = (name, attribute) => {
1820
1958
  const { type, args = [], ...opts } = getColumnType(attribute);
1821
1959
  return {
1822
- name,
1960
+ name: identifiers.getName(name),
1823
1961
  type,
1824
1962
  args,
1825
1963
  defaultTo: null,
@@ -1841,8 +1979,8 @@ const createTable = (meta) => {
1841
1979
  if (attribute.type === "relation") {
1842
1980
  if ("morphColumn" in attribute && attribute.morphColumn && attribute.owner) {
1843
1981
  const { idColumn, typeColumn } = attribute.morphColumn;
1844
- const idColumnName = getName(idColumn.name);
1845
- const typeColumnName = getName(typeColumn.name);
1982
+ const idColumnName = identifiers.getName(idColumn.name);
1983
+ const typeColumnName = identifiers.getName(typeColumn.name);
1846
1984
  table.columns.push(
1847
1985
  createColumn(idColumnName, {
1848
1986
  type: "integer",
@@ -1859,7 +1997,7 @@ const createTable = (meta) => {
1859
1997
  referencedTable,
1860
1998
  columnType = "integer"
1861
1999
  } = attribute.joinColumn;
1862
- const columnName = getName(columnNameFull);
2000
+ const columnName = identifiers.getName(columnNameFull);
1863
2001
  const column = createColumn(columnName, {
1864
2002
  // TODO: find the column type automatically, or allow passing all the column params
1865
2003
  type: columnType,
@@ -1868,7 +2006,7 @@ const createTable = (meta) => {
1868
2006
  }
1869
2007
  });
1870
2008
  table.columns.push(column);
1871
- const fkName = getFkIndexName([table.name, columnName]);
2009
+ const fkName = identifiers.getFkIndexName([table.name, columnName]);
1872
2010
  table.foreignKeys.push({
1873
2011
  name: fkName,
1874
2012
  columns: [column.name],
@@ -1883,19 +2021,19 @@ const createTable = (meta) => {
1883
2021
  });
1884
2022
  }
1885
2023
  } else if (isScalarAttribute(attribute)) {
1886
- const columnName = getName(attribute.columnName || key);
2024
+ const columnName = identifiers.getName(attribute.columnName || key);
1887
2025
  const column = createColumn(columnName, attribute);
1888
2026
  if (column.unique) {
1889
2027
  table.indexes.push({
1890
2028
  type: "unique",
1891
- name: getUniqueIndexName([table.name, column.name]),
2029
+ name: identifiers.getUniqueIndexName([table.name, column.name]),
1892
2030
  columns: [columnName]
1893
2031
  });
1894
2032
  }
1895
2033
  if (column.primary) {
1896
2034
  table.indexes.push({
1897
2035
  type: "primary",
1898
- name: getPrimaryIndexName([table.name, column.name]),
2036
+ name: identifiers.getPrimaryIndexName([table.name, column.name]),
1899
2037
  columns: [columnName]
1900
2038
  });
1901
2039
  }
@@ -1995,12 +2133,13 @@ const metadataToSchema = (metadata) => {
1995
2133
  });
1996
2134
  return schema;
1997
2135
  };
1998
- const debug = createDebug("strapi::database");
2136
+ const debug$1 = createDebug("strapi::database");
1999
2137
  const createSchemaProvider = (db) => {
2000
2138
  const state = {};
2001
2139
  return {
2002
2140
  get schema() {
2003
2141
  if (!state.schema) {
2142
+ debug$1("Converting metadata to database schema");
2004
2143
  state.schema = metadataToSchema(db.metadata);
2005
2144
  }
2006
2145
  return state.schema;
@@ -2012,7 +2151,7 @@ const createSchemaProvider = (db) => {
2012
2151
  * Drops the database schema
2013
2152
  */
2014
2153
  async drop() {
2015
- debug("Dropping database schema");
2154
+ debug$1("Dropping database schema");
2016
2155
  const DBSchema = await db.dialect.schemaInspector.getSchema();
2017
2156
  await this.builder.dropSchema(DBSchema);
2018
2157
  },
@@ -2020,19 +2159,19 @@ const createSchemaProvider = (db) => {
2020
2159
  * Creates the database schema
2021
2160
  */
2022
2161
  async create() {
2023
- debug("Created database schema");
2162
+ debug$1("Created database schema");
2024
2163
  await this.builder.createSchema(this.schema);
2025
2164
  },
2026
2165
  /**
2027
2166
  * Resets the database schema
2028
2167
  */
2029
2168
  async reset() {
2030
- debug("Resetting database schema");
2169
+ debug$1("Resetting database schema");
2031
2170
  await this.drop();
2032
2171
  await this.create();
2033
2172
  },
2034
2173
  async syncSchema() {
2035
- debug("Synchronizing database schema");
2174
+ debug$1("Synchronizing database schema");
2036
2175
  const DBSchema = await db.dialect.schemaInspector.getSchema();
2037
2176
  const { status, diff } = await this.schemaDiff.diff(DBSchema, this.schema);
2038
2177
  if (status === "CHANGED") {
@@ -2045,28 +2184,28 @@ const createSchemaProvider = (db) => {
2045
2184
  // TODO: Allow keeping extra indexes / extra tables / extra columns (globally or on a per table basis)
2046
2185
  async sync() {
2047
2186
  if (await db.migrations.shouldRun()) {
2048
- debug("Found migrations to run");
2187
+ debug$1("Found migrations to run");
2049
2188
  await db.migrations.up();
2050
2189
  return this.syncSchema();
2051
2190
  }
2052
2191
  const oldSchema = await this.schemaStorage.read();
2053
2192
  if (!oldSchema) {
2054
- debug("Schema not persisted yet");
2193
+ debug$1("Schema not persisted yet");
2055
2194
  return this.syncSchema();
2056
2195
  }
2057
2196
  const { hash: oldHash } = oldSchema;
2058
2197
  const hash = await this.schemaStorage.hashSchema(this.schema);
2059
2198
  if (oldHash !== hash) {
2060
- debug("Schema changed");
2199
+ debug$1("Schema changed");
2061
2200
  return this.syncSchema();
2062
2201
  }
2063
- debug("Schema unchanged");
2202
+ debug$1("Schema unchanged");
2064
2203
  }
2065
2204
  };
2066
2205
  };
2067
- const ID = ID_COLUMN;
2068
- const ORDER = ORDER_COLUMN;
2069
- const FIELD = FIELD_COLUMN;
2206
+ const ID = identifiers.ID_COLUMN;
2207
+ const ORDER = identifiers.ORDER_COLUMN;
2208
+ const FIELD = identifiers.FIELD_COLUMN;
2070
2209
  const hasInversedBy = (attr) => "inversedBy" in attr;
2071
2210
  const hasMappedBy = (attr) => "mappedBy" in attr;
2072
2211
  const isOneToAny = (attribute) => ["oneToOne", "oneToMany"].includes(attribute.relation);
@@ -2087,7 +2226,7 @@ const createOneToOne = (attributeName, attribute, meta, metadata) => {
2087
2226
  meta
2088
2227
  });
2089
2228
  } else {
2090
- createJoinColum(metadata, {
2229
+ createJoinColumn(metadata, {
2091
2230
  attribute,
2092
2231
  attributeName,
2093
2232
  meta
@@ -2117,7 +2256,7 @@ const createManyToOne = (attributeName, attribute, meta, metadata) => {
2117
2256
  meta
2118
2257
  });
2119
2258
  } else {
2120
- createJoinColum(metadata, {
2259
+ createJoinColumn(metadata, {
2121
2260
  attribute,
2122
2261
  attributeName,
2123
2262
  meta
@@ -2134,15 +2273,11 @@ const createManyToMany = (attributeName, attribute, meta, metadata) => {
2134
2273
  }
2135
2274
  };
2136
2275
  const createMorphToOne = (attributeName, attribute) => {
2137
- const idColumnName = getJoinColumnAttributeIdName("target");
2138
- const typeColumnName = getMorphColumnTypeName("target");
2139
- if ("morphColumn" in attribute && attribute.morphColumn) {
2140
- return;
2141
- }
2276
+ const idColumnName = identifiers.getJoinColumnAttributeIdName("target");
2277
+ const typeColumnName = identifiers.getMorphColumnTypeName("target");
2142
2278
  Object.assign(attribute, {
2143
2279
  owner: true,
2144
- morphColumn: {
2145
- // TODO: add referenced column
2280
+ morphColumn: attribute.morphColumn ?? {
2146
2281
  typeColumn: {
2147
2282
  name: typeColumnName
2148
2283
  },
@@ -2157,11 +2292,11 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => {
2157
2292
  if ("joinTable" in attribute && attribute.joinTable) {
2158
2293
  return;
2159
2294
  }
2160
- const joinTableName = getMorphTableName(meta.tableName, attributeName);
2161
- const joinColumnName = getMorphColumnJoinTableIdName(meta.singularName);
2162
- const idColumnName = getMorphColumnAttributeIdName(attributeName);
2163
- const typeColumnName = getMorphColumnTypeName(attributeName);
2164
- const fkIndexName = getFkIndexName(joinTableName);
2295
+ const joinTableName = identifiers.getMorphTableName(meta.tableName, attributeName);
2296
+ const joinColumnName = identifiers.getMorphColumnJoinTableIdName(snakeCase(meta.singularName));
2297
+ const idColumnName = identifiers.getMorphColumnAttributeIdName(attributeName);
2298
+ const typeColumnName = identifiers.getMorphColumnTypeName(attributeName);
2299
+ const fkIndexName = identifiers.getFkIndexName(joinTableName);
2165
2300
  metadata.add({
2166
2301
  singularName: joinTableName,
2167
2302
  uid: joinTableName,
@@ -2174,7 +2309,9 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => {
2174
2309
  type: "integer",
2175
2310
  column: {
2176
2311
  unsigned: true
2177
- }
2312
+ },
2313
+ // This must be set explicitly so that it is used instead of shortening the attribute name, which is already shortened
2314
+ columnName: joinColumnName
2178
2315
  },
2179
2316
  [idColumnName]: {
2180
2317
  type: "integer",
@@ -2201,11 +2338,11 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => {
2201
2338
  columns: [joinColumnName]
2202
2339
  },
2203
2340
  {
2204
- name: `${joinTableName}_order_index`,
2341
+ name: identifiers.getOrderIndexName(joinTableName),
2205
2342
  columns: [ORDER]
2206
2343
  },
2207
2344
  {
2208
- name: `${joinTableName}_id_column_index`,
2345
+ name: identifiers.getIdColumnIndexName(joinTableName),
2209
2346
  columns: [idColumnName]
2210
2347
  }
2211
2348
  ],
@@ -2214,7 +2351,7 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => {
2214
2351
  name: fkIndexName,
2215
2352
  columns: [joinColumnName],
2216
2353
  referencedColumns: [ID],
2217
- referencedTable: getTableName(meta.tableName),
2354
+ referencedTable: meta.tableName,
2218
2355
  onDelete: "CASCADE"
2219
2356
  }
2220
2357
  ],
@@ -2261,12 +2398,12 @@ const createMorphMany = (attributeName, attribute, meta, metadata) => {
2261
2398
  throw new Error(`Morph target attribute not found. Looking for ${attribute.morphBy}`);
2262
2399
  }
2263
2400
  };
2264
- const createJoinColum = (metadata, { attribute, attributeName }) => {
2401
+ const createJoinColumn = (metadata, { attribute, attributeName }) => {
2265
2402
  const targetMeta = metadata.get(attribute.target);
2266
2403
  if (!targetMeta) {
2267
2404
  throw new Error(`Unknown target ${attribute.target}`);
2268
2405
  }
2269
- const joinColumnName = _.snakeCase(`${attributeName}_id`);
2406
+ const joinColumnName = identifiers.getJoinColumnAttributeIdName(snakeCase(attributeName));
2270
2407
  const joinColumn = {
2271
2408
  name: joinColumnName,
2272
2409
  referencedColumn: ID,
@@ -2281,7 +2418,7 @@ const createJoinColum = (metadata, { attribute, attributeName }) => {
2281
2418
  Object.assign(inverseAttribute, {
2282
2419
  joinColumn: {
2283
2420
  name: joinColumn.referencedColumn,
2284
- referencedColumn: joinColumn.name
2421
+ referencedColumn: joinColumnName
2285
2422
  }
2286
2423
  });
2287
2424
  }
@@ -2294,21 +2431,26 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
2294
2431
  if ("joinTable" in attribute && attribute.joinTable) {
2295
2432
  return;
2296
2433
  }
2297
- const joinTableName = getJoinTableName(meta.tableName, attributeName);
2298
- const joinColumnName = getJoinColumnAttributeIdName(meta.singularName);
2299
- let inverseJoinColumnName = getJoinColumnAttributeIdName(targetMeta.singularName);
2434
+ const joinTableName = identifiers.getJoinTableName(
2435
+ snakeCase(meta.tableName),
2436
+ snakeCase(attributeName)
2437
+ );
2438
+ const joinColumnName = identifiers.getJoinColumnAttributeIdName(snakeCase(meta.singularName));
2439
+ let inverseJoinColumnName = identifiers.getJoinColumnAttributeIdName(
2440
+ snakeCase(targetMeta.singularName)
2441
+ );
2300
2442
  if (joinColumnName === inverseJoinColumnName) {
2301
- inverseJoinColumnName = getInverseJoinColumnAttributeIdName(
2302
- targetMeta.singularName
2443
+ inverseJoinColumnName = identifiers.getInverseJoinColumnAttributeIdName(
2444
+ snakeCase(targetMeta.singularName)
2303
2445
  );
2304
2446
  }
2305
- const orderColumnName = getOrderColumnName(targetMeta.singularName);
2306
- let inverseOrderColumnName = getOrderColumnName(meta.singularName);
2447
+ const orderColumnName = identifiers.getOrderColumnName(snakeCase(targetMeta.singularName));
2448
+ let inverseOrderColumnName = identifiers.getOrderColumnName(snakeCase(meta.singularName));
2307
2449
  if (attribute.relation === "manyToMany" && orderColumnName === inverseOrderColumnName) {
2308
- inverseOrderColumnName = getInverseOrderColumnName(meta.singularName);
2450
+ inverseOrderColumnName = identifiers.getInverseOrderColumnName(snakeCase(meta.singularName));
2309
2451
  }
2310
- const fkIndexName = getFkIndexName(joinTableName);
2311
- const invFkIndexName = getInverseFkIndexName(joinTableName);
2452
+ const fkIndexName = identifiers.getFkIndexName(joinTableName);
2453
+ const invFkIndexName = identifiers.getInverseFkIndexName(joinTableName);
2312
2454
  const metadataSchema = {
2313
2455
  singularName: joinTableName,
2314
2456
  uid: joinTableName,
@@ -2321,13 +2463,17 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
2321
2463
  type: "integer",
2322
2464
  column: {
2323
2465
  unsigned: true
2324
- }
2466
+ },
2467
+ // This must be set explicitly so that it is used instead of shortening the attribute name, which is already shortened
2468
+ columnName: joinColumnName
2325
2469
  },
2326
2470
  [inverseJoinColumnName]: {
2327
2471
  type: "integer",
2328
2472
  column: {
2329
2473
  unsigned: true
2330
- }
2474
+ },
2475
+ // This must be set explicitly so that it is used instead of shortening the attribute name, which is already shortened
2476
+ columnName: inverseJoinColumnName
2331
2477
  }
2332
2478
  // TODO: add extra pivot attributes -> user should use an intermediate entity
2333
2479
  },
@@ -2341,7 +2487,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
2341
2487
  columns: [inverseJoinColumnName]
2342
2488
  },
2343
2489
  {
2344
- name: getUniqueIndexName(joinTableName),
2490
+ name: identifiers.getUniqueIndexName(joinTableName),
2345
2491
  columns: [joinColumnName, inverseJoinColumnName],
2346
2492
  type: "unique"
2347
2493
  }
@@ -2352,7 +2498,6 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
2352
2498
  columns: [joinColumnName],
2353
2499
  referencedColumns: [ID],
2354
2500
  referencedTable: meta.tableName,
2355
- // TODO: does this need to be wrapped or do we trust meta.tableName to be the right shortened version?
2356
2501
  onDelete: "CASCADE"
2357
2502
  },
2358
2503
  {
@@ -2360,7 +2505,6 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
2360
2505
  columns: [inverseJoinColumnName],
2361
2506
  referencedColumns: [ID],
2362
2507
  referencedTable: targetMeta.tableName,
2363
- // TODO: does this need to be wrapped or do we trust meta.tableName to be the right shortened version?
2364
2508
  onDelete: "CASCADE"
2365
2509
  }
2366
2510
  ],
@@ -2390,8 +2534,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
2390
2534
  }
2391
2535
  };
2392
2536
  metadataSchema.indexes.push({
2393
- name: getOrderFkIndexName(joinTableName),
2394
- // TODO: should we send joinTableName as parts?
2537
+ name: identifiers.getOrderFkIndexName(joinTableName),
2395
2538
  columns: [orderColumnName]
2396
2539
  });
2397
2540
  joinTable.orderColumnName = orderColumnName;
@@ -2406,7 +2549,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
2406
2549
  }
2407
2550
  };
2408
2551
  metadataSchema.indexes.push({
2409
- name: getOrderInverseFkIndexName(joinTableName),
2552
+ name: identifiers.getOrderInverseFkIndexName(joinTableName),
2410
2553
  columns: [inverseOrderColumnName]
2411
2554
  });
2412
2555
  joinTable.inverseOrderColumnName = inverseOrderColumnName;
@@ -2464,6 +2607,12 @@ const createRelation = (attributeName, attribute, meta, metadata) => {
2464
2607
  }
2465
2608
  };
2466
2609
  class Metadata extends Map {
2610
+ // TODO: we expose the global identifiers in this way so that in the future we can instantiate our own
2611
+ // However, it should NOT be done until all the methods used by metadata can be part of this metadata object
2612
+ // and access this one; currently they all access the global identifiers directly.
2613
+ get identifiers() {
2614
+ return identifiers;
2615
+ }
2467
2616
  get(key) {
2468
2617
  if (!super.has(key)) {
2469
2618
  throw new Error(`Metadata for "${key}" not found`);
@@ -2487,10 +2636,12 @@ class Metadata extends Map {
2487
2636
  seenTables.set(meta.tableName, true);
2488
2637
  }
2489
2638
  }
2490
- loadModels(models = []) {
2491
- for (const model of _.cloneDeep(models)) {
2639
+ loadModels(models) {
2640
+ for (const model of cloneDeep(models ?? [])) {
2641
+ const tableName = identifiers.getTableName(model.tableName);
2492
2642
  this.add({
2493
2643
  ...model,
2644
+ tableName,
2494
2645
  attributes: {
2495
2646
  ...model.attributes
2496
2647
  },
@@ -2531,10 +2682,13 @@ class Metadata extends Map {
2531
2682
  }
2532
2683
  }
2533
2684
  const createAttribute = (attributeName, attribute) => {
2534
- const columnName = getColumnName(attributeName);
2685
+ if ("columnName" in attribute && attribute.columnName) {
2686
+ return;
2687
+ }
2688
+ const columnName = identifiers.getColumnName(snakeCase(attributeName));
2535
2689
  Object.assign(attribute, { columnName });
2536
2690
  };
2537
- const createMetadata = (models = []) => {
2691
+ const createMetadata = (models) => {
2538
2692
  const metadata = new Metadata();
2539
2693
  if (models.length) {
2540
2694
  metadata.loadModels(models);
@@ -2885,11 +3039,54 @@ const createPivotJoin = (ctx, { alias, refAlias, joinTable, targetMeta }) => {
2885
3039
  return subAlias;
2886
3040
  };
2887
3041
  const createJoin = (ctx, { alias, refAlias, attributeName, attribute }) => {
2888
- const { db, qb } = ctx;
3042
+ const { db, qb, uid } = ctx;
2889
3043
  if (attribute.type !== "relation") {
2890
3044
  throw new Error(`Cannot join on non relational field ${attributeName}`);
2891
3045
  }
2892
3046
  const targetMeta = db.metadata.get(attribute.target);
3047
+ if (["morphOne", "morphMany"].includes(attribute.relation)) {
3048
+ const targetAttribute = targetMeta.attributes[attribute.morphBy];
3049
+ const { joinTable: joinTable2, morphColumn } = targetAttribute;
3050
+ if (morphColumn) {
3051
+ const subAlias = refAlias || qb.getAlias();
3052
+ qb.join({
3053
+ alias: subAlias,
3054
+ referencedTable: targetMeta.tableName,
3055
+ referencedColumn: morphColumn.idColumn.name,
3056
+ rootColumn: morphColumn.idColumn.referencedColumn,
3057
+ rootTable: alias,
3058
+ on: {
3059
+ [morphColumn.typeColumn.name]: uid,
3060
+ ...morphColumn.on
3061
+ }
3062
+ });
3063
+ return subAlias;
3064
+ }
3065
+ if (joinTable2) {
3066
+ const joinAlias = qb.getAlias();
3067
+ qb.join({
3068
+ alias: joinAlias,
3069
+ referencedTable: joinTable2.name,
3070
+ referencedColumn: joinTable2.morphColumn.idColumn.name,
3071
+ rootColumn: joinTable2.morphColumn.idColumn.referencedColumn,
3072
+ rootTable: alias,
3073
+ on: {
3074
+ [joinTable2.morphColumn.typeColumn.name]: uid,
3075
+ field: attributeName
3076
+ }
3077
+ });
3078
+ const subAlias = refAlias || qb.getAlias();
3079
+ qb.join({
3080
+ alias: subAlias,
3081
+ referencedTable: targetMeta.tableName,
3082
+ referencedColumn: joinTable2.joinColumn.referencedColumn,
3083
+ rootColumn: joinTable2.joinColumn.name,
3084
+ rootTable: joinAlias
3085
+ });
3086
+ return subAlias;
3087
+ }
3088
+ return alias;
3089
+ }
2893
3090
  const { joinColumn } = attribute;
2894
3091
  if (joinColumn) {
2895
3092
  const subAlias = refAlias || qb.getAlias();
@@ -2983,6 +3180,7 @@ const processOrderBy = (orderBy, ctx) => {
2983
3180
  }
2984
3181
  throw new Error("Invalid orderBy syntax");
2985
3182
  };
3183
+ const joinColPrefix = "__strapi";
2986
3184
  const XtoOne = async (input, ctx) => {
2987
3185
  const { attribute, attributeName, results, populateValue, targetMeta, isCount } = input;
2988
3186
  const { db, qb } = ctx;
@@ -3011,6 +3209,8 @@ const XtoOne = async (input, ctx) => {
3011
3209
  const { name: joinColumnName, referencedColumn: referencedColumnName } = joinTable.joinColumn;
3012
3210
  const alias = qb2.getAlias();
3013
3211
  const joinColAlias = `${alias}.${joinColumnName}`;
3212
+ const joinColRenameAs = `${joinColPrefix}${joinColumnName}`;
3213
+ const joinColSelect = `${joinColAlias} as ${joinColRenameAs}`;
3014
3214
  const referencedValues = _.uniq(
3015
3215
  results.map((r) => r[referencedColumnName]).filter((value) => !_.isNil(value))
3016
3216
  );
@@ -3029,10 +3229,13 @@ const XtoOne = async (input, ctx) => {
3029
3229
  rootTable: qb2.alias,
3030
3230
  on: joinTable.on
3031
3231
  }).select([joinColAlias, qb2.raw("count(*) AS count")]).where({ [joinColAlias]: referencedValues }).groupBy(joinColAlias).execute({ mapResults: false });
3032
- const map22 = rows2.reduce((map3, row) => {
3033
- map3[row[joinColumnName]] = { count: Number(row.count) };
3034
- return map3;
3035
- }, {});
3232
+ const map22 = rows2.reduce(
3233
+ (map3, row) => {
3234
+ map3[row[joinColumnName]] = { count: Number(row.count) };
3235
+ return map3;
3236
+ },
3237
+ {}
3238
+ );
3036
3239
  results.forEach((result) => {
3037
3240
  result[attributeName] = map22[result[referencedColumnName]] || { count: 0 };
3038
3241
  });
@@ -3052,8 +3255,8 @@ const XtoOne = async (input, ctx) => {
3052
3255
  rootTable: qb2.alias,
3053
3256
  on: joinTable.on,
3054
3257
  orderBy: joinTable.orderBy
3055
- }).addSelect(joinColAlias).where({ [joinColAlias]: referencedValues }).execute({ mapResults: false });
3056
- const map2 = _.groupBy(joinColumnName)(rows);
3258
+ }).addSelect(joinColSelect).where({ [joinColAlias]: referencedValues }).execute({ mapResults: false });
3259
+ const map2 = _.groupBy(joinColRenameAs)(rows);
3057
3260
  results.forEach((result) => {
3058
3261
  result[attributeName] = fromTargetRow(_.first(map2[result[referencedColumnName]]));
3059
3262
  });
@@ -3087,6 +3290,8 @@ const oneToMany = async (input, ctx) => {
3087
3290
  const { name: joinColumnName, referencedColumn: referencedColumnName } = joinTable.joinColumn;
3088
3291
  const alias = qb2.getAlias();
3089
3292
  const joinColAlias = `${alias}.${joinColumnName}`;
3293
+ const joinColRenameAs = `${joinColPrefix}${joinColumnName}`;
3294
+ const joinColSelect = `${joinColAlias} as ${joinColRenameAs}`;
3090
3295
  const referencedValues = _.uniq(
3091
3296
  results.map((r) => r[referencedColumnName]).filter((value) => !_.isNil(value))
3092
3297
  );
@@ -3104,11 +3309,14 @@ const oneToMany = async (input, ctx) => {
3104
3309
  rootColumn: joinTable.inverseJoinColumn.referencedColumn,
3105
3310
  rootTable: qb2.alias,
3106
3311
  on: joinTable.on
3107
- }).select([joinColAlias, qb2.raw("count(*) AS count")]).where({ [joinColAlias]: referencedValues }).groupBy(joinColAlias).execute({ mapResults: false });
3108
- const map22 = rows2.reduce((map3, row) => {
3109
- map3[row[joinColumnName]] = { count: Number(row.count) };
3110
- return map3;
3111
- }, {});
3312
+ }).select([joinColSelect, qb2.raw("count(*) AS count")]).where({ [joinColAlias]: referencedValues }).groupBy(joinColAlias).execute({ mapResults: false });
3313
+ const map22 = rows2.reduce(
3314
+ (map3, row) => {
3315
+ map3[row[joinColRenameAs]] = { count: Number(row.count) };
3316
+ return map3;
3317
+ },
3318
+ {}
3319
+ );
3112
3320
  results.forEach((result) => {
3113
3321
  result[attributeName] = map22[result[referencedColumnName]] || { count: 0 };
3114
3322
  });
@@ -3128,8 +3336,8 @@ const oneToMany = async (input, ctx) => {
3128
3336
  rootTable: qb2.alias,
3129
3337
  on: joinTable.on,
3130
3338
  orderBy: _.mapValues((v) => populateValue.ordering || v, joinTable.orderBy)
3131
- }).addSelect(joinColAlias).where({ [joinColAlias]: referencedValues }).execute({ mapResults: false });
3132
- const map2 = _.groupBy(joinColumnName)(rows);
3339
+ }).addSelect(joinColSelect).where({ [joinColAlias]: referencedValues }).execute({ mapResults: false });
3340
+ const map2 = _.groupBy(joinColRenameAs)(rows);
3133
3341
  results.forEach((r) => {
3134
3342
  r[attributeName] = fromTargetRow(map2[r[referencedColumnName]] || []);
3135
3343
  });
@@ -3144,6 +3352,8 @@ const manyToMany = async (input, ctx) => {
3144
3352
  const { name: joinColumnName, referencedColumn: referencedColumnName } = joinTable.joinColumn;
3145
3353
  const alias = populateQb.getAlias();
3146
3354
  const joinColAlias = `${alias}.${joinColumnName}`;
3355
+ const joinColRenameAs = `${joinColPrefix}${joinColumnName}`;
3356
+ const joinColSelect = `${joinColAlias} as ${joinColRenameAs}`;
3147
3357
  const referencedValues = _.uniq(
3148
3358
  results.map((r) => r[referencedColumnName]).filter((value) => !_.isNil(value))
3149
3359
  );
@@ -3162,10 +3372,13 @@ const manyToMany = async (input, ctx) => {
3162
3372
  rootTable: populateQb.alias,
3163
3373
  on: joinTable.on
3164
3374
  }).select([joinColAlias, populateQb.raw("count(*) AS count")]).where({ [joinColAlias]: referencedValues }).groupBy(joinColAlias).execute({ mapResults: false });
3165
- const map22 = rows2.reduce((map3, row) => {
3166
- map3[row[joinColumnName]] = { count: Number(row.count) };
3167
- return map3;
3168
- }, {});
3375
+ const map22 = rows2.reduce(
3376
+ (map3, row) => {
3377
+ map3[row[joinColumnName]] = { count: Number(row.count) };
3378
+ return map3;
3379
+ },
3380
+ {}
3381
+ );
3169
3382
  results.forEach((result) => {
3170
3383
  result[attributeName] = map22[result[referencedColumnName]] || { count: 0 };
3171
3384
  });
@@ -3185,8 +3398,8 @@ const manyToMany = async (input, ctx) => {
3185
3398
  rootTable: populateQb.alias,
3186
3399
  on: joinTable.on,
3187
3400
  orderBy: _.mapValues((v) => populateValue.ordering || v, joinTable.orderBy)
3188
- }).addSelect(joinColAlias).where({ [joinColAlias]: referencedValues }).execute({ mapResults: false });
3189
- const map2 = _.groupBy(joinColumnName)(rows);
3401
+ }).addSelect(joinColSelect).where({ [joinColAlias]: referencedValues }).execute({ mapResults: false });
3402
+ const map2 = _.groupBy(joinColRenameAs)(rows);
3190
3403
  results.forEach((result) => {
3191
3404
  result[attributeName] = fromTargetRow(map2[result[referencedColumnName]] || []);
3192
3405
  });
@@ -3539,6 +3752,31 @@ const processNested = (where, ctx) => {
3539
3752
  }
3540
3753
  return processWhere(where, ctx);
3541
3754
  };
3755
+ const processRelationWhere = (where, ctx) => {
3756
+ const { qb, alias } = ctx;
3757
+ const idAlias = qb.aliasColumn("id", alias);
3758
+ if (!isRecord$1(where)) {
3759
+ return { [idAlias]: where };
3760
+ }
3761
+ const keys = Object.keys(where);
3762
+ const operatorKeys = keys.filter((key) => isOperator(key));
3763
+ if (operatorKeys.length > 0 && operatorKeys.length !== keys.length) {
3764
+ throw new Error(`Operator and non-operator keys cannot be mixed in a relation where clause`);
3765
+ }
3766
+ if (operatorKeys.length > 1) {
3767
+ throw new Error(
3768
+ `Only one operator key is allowed in a relation where clause, but found: ${operatorKeys}`
3769
+ );
3770
+ }
3771
+ if (operatorKeys.length === 1) {
3772
+ const operator = operatorKeys[0];
3773
+ if (isOperatorOfType("group", operator)) {
3774
+ return processWhere(where, ctx);
3775
+ }
3776
+ return { [idAlias]: { [operator]: processNested(where[operator], ctx) } };
3777
+ }
3778
+ return processWhere(where, ctx);
3779
+ };
3542
3780
  function processWhere(where, ctx) {
3543
3781
  if (!isArray(where) && !isRecord$1(where)) {
3544
3782
  throw new Error("Where must be an array or an object");
@@ -3551,7 +3789,10 @@ function processWhere(where, ctx) {
3551
3789
  const filters = {};
3552
3790
  for (const key of Object.keys(where)) {
3553
3791
  const value = where[key];
3554
- if (isOperatorOfType("group", key) && Array.isArray(value)) {
3792
+ if (isOperatorOfType("group", key)) {
3793
+ if (!Array.isArray(value)) {
3794
+ throw new Error(`Operator ${key} must be an array`);
3795
+ }
3555
3796
  filters[key] = value.map((sub) => processNested(sub, ctx));
3556
3797
  continue;
3557
3798
  }
@@ -3575,15 +3816,12 @@ function processWhere(where, ctx) {
3575
3816
  attributeName: key,
3576
3817
  attribute
3577
3818
  });
3578
- let nestedWhere = processNested(value, {
3819
+ const nestedWhere = processRelationWhere(value, {
3579
3820
  db,
3580
3821
  qb,
3581
3822
  alias: subAlias,
3582
3823
  uid: attribute.target
3583
3824
  });
3584
- if (!isRecord$1(nestedWhere) || isOperatorOfType("where", keys(nestedWhere)[0])) {
3585
- nestedWhere = { [qb.aliasColumn("id", subAlias)]: nestedWhere };
3586
- }
3587
3825
  Object.assign(filters, nestedWhere);
3588
3826
  continue;
3589
3827
  }
@@ -3736,8 +3974,8 @@ const applyWhereToColumn = (qb, column, columnWhere) => {
3736
3974
  }
3737
3975
  return qb.where(column, columnWhere);
3738
3976
  }
3739
- const keys2 = Object.keys(columnWhere);
3740
- keys2.forEach((operator) => {
3977
+ const keys = Object.keys(columnWhere);
3978
+ keys.forEach((operator) => {
3741
3979
  const value = columnWhere[operator];
3742
3980
  applyOperator(qb, column, operator, value);
3743
3981
  });
@@ -4676,21 +4914,24 @@ const sortConnectArray = (connectArr, initialArr = [], strictSort = true) => {
4676
4914
  (acc, rel) => ({ ...acc, [rel.id]: true }),
4677
4915
  {}
4678
4916
  );
4679
- const mappedRelations = connectArr.reduce((mapper, relation) => {
4680
- const adjacentRelId = relation.position?.before || relation.position?.after;
4681
- if (!adjacentRelId || !relationInInitialArray[adjacentRelId] && !mapper[adjacentRelId]) {
4682
- needsSorting = true;
4683
- }
4684
- if (mapper[relation.id]) {
4685
- throw new InvalidRelationError(
4686
- `The relation with id ${relation.id} is already connected. You cannot connect the same relation twice.`
4687
- );
4688
- }
4689
- return {
4690
- [relation.id]: { ...relation, computed: false },
4691
- ...mapper
4692
- };
4693
- }, {});
4917
+ const mappedRelations = connectArr.reduce(
4918
+ (mapper, relation) => {
4919
+ const adjacentRelId = relation.position?.before || relation.position?.after;
4920
+ if (!adjacentRelId || !relationInInitialArray[adjacentRelId] && !mapper[adjacentRelId]) {
4921
+ needsSorting = true;
4922
+ }
4923
+ if (mapper[relation.id]) {
4924
+ throw new InvalidRelationError(
4925
+ `The relation with id ${relation.id} is already connected. You cannot connect the same relation twice.`
4926
+ );
4927
+ }
4928
+ return {
4929
+ [relation.id]: { ...relation, computed: false },
4930
+ ...mapper
4931
+ };
4932
+ },
4933
+ {}
4934
+ );
4694
4935
  if (!needsSorting)
4695
4936
  return connectArr;
4696
4937
  const computeRelation = (relation, relationsSeenInBranch) => {
@@ -4798,14 +5039,17 @@ const relationsOrderer = (initArr, idColumn, orderColumn, strict2) => {
4798
5039
  * Get a map between the relation id and its order
4799
5040
  */
4800
5041
  getOrderMap() {
4801
- return _$1(computedRelations).groupBy("order").reduce((acc, relations) => {
4802
- if (relations[0]?.init)
5042
+ return _$1(computedRelations).groupBy("order").reduce(
5043
+ (acc, relations) => {
5044
+ if (relations[0]?.init)
5045
+ return acc;
5046
+ relations.forEach((relation, idx) => {
5047
+ acc[relation.id] = Math.floor(relation.order) + (idx + 1) / (relations.length + 1);
5048
+ });
4803
5049
  return acc;
4804
- relations.forEach((relation, idx) => {
4805
- acc[relation.id] = Math.floor(relation.order) + (idx + 1) / (relations.length + 1);
4806
- });
4807
- return acc;
4808
- }, {});
5050
+ },
5051
+ {}
5052
+ );
4809
5053
  }
4810
5054
  };
4811
5055
  };
@@ -5559,10 +5803,13 @@ const createEntityManager = (db) => {
5559
5803
  const entry = await this.findOne(uid, {
5560
5804
  select: ["id"],
5561
5805
  where: { id: entity.id },
5562
- populate: fieldsArr.reduce((acc, field) => {
5563
- acc[field] = populate || true;
5564
- return acc;
5565
- }, {})
5806
+ populate: fieldsArr.reduce(
5807
+ (acc, field) => {
5808
+ acc[field] = populate || true;
5809
+ return acc;
5810
+ },
5811
+ {}
5812
+ )
5566
5813
  });
5567
5814
  if (!entry) {
5568
5815
  return null;
@@ -5628,7 +5875,7 @@ const createStorage = (opts) => {
5628
5875
  };
5629
5876
  };
5630
5877
  const wrapTransaction = (db) => (fn) => () => {
5631
- return db.connection.transaction((trx) => Promise.resolve(fn(trx, db)));
5878
+ return db.transaction(({ trx }) => Promise.resolve(fn(trx, db)));
5632
5879
  };
5633
5880
  const migrationResolver = ({ name, path: path2, context }) => {
5634
5881
  const { db } = context;
@@ -5678,14 +5925,314 @@ const createUserMigrationProvider = (db) => {
5678
5925
  }
5679
5926
  };
5680
5927
  };
5681
- const internalMigrations = [];
5928
+ const QUERIES = {
5929
+ async postgres(knex2, params) {
5930
+ const res = await knex2.raw(
5931
+ `
5932
+ SELECT :tableName:.id as id, string_agg(DISTINCT :inverseJoinColumn:::character varying, ',') as other_ids
5933
+ FROM :tableName:
5934
+ LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:
5935
+ WHERE document_id IS NULL
5936
+ GROUP BY :tableName:.id, :joinColumn:
5937
+ LIMIT 1;
5938
+ `,
5939
+ params
5940
+ );
5941
+ return res.rows;
5942
+ },
5943
+ async mysql(knex2, params) {
5944
+ const [res] = await knex2.raw(
5945
+ `
5946
+ SELECT :tableName:.id as id, group_concat(DISTINCT :inverseJoinColumn:) as other_ids
5947
+ FROM :tableName:
5948
+ LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:
5949
+ WHERE document_id IS NULL
5950
+ GROUP BY :tableName:.id, :joinColumn:
5951
+ LIMIT 1;
5952
+ `,
5953
+ params
5954
+ );
5955
+ return res;
5956
+ },
5957
+ async sqlite(knex2, params) {
5958
+ return knex2.raw(
5959
+ `
5960
+ SELECT :tableName:.id as id, group_concat(DISTINCT :inverseJoinColumn:) as other_ids
5961
+ FROM :tableName:
5962
+ LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:
5963
+ WHERE document_id IS NULL
5964
+ GROUP BY :joinColumn:
5965
+ LIMIT 1;
5966
+ `,
5967
+ params
5968
+ );
5969
+ }
5970
+ };
5971
+ const getNextIdsToCreateDocumentId = async (db, knex2, {
5972
+ joinColumn,
5973
+ inverseJoinColumn,
5974
+ tableName,
5975
+ joinTableName
5976
+ }) => {
5977
+ const res = await QUERIES[db.dialect.client](knex2, {
5978
+ joinColumn,
5979
+ inverseJoinColumn,
5980
+ tableName,
5981
+ joinTableName
5982
+ });
5983
+ if (res.length > 0) {
5984
+ const row = res[0];
5985
+ const otherIds = row.other_ids ? row.other_ids.split(",").map((v) => parseInt(v, 10)) : [];
5986
+ return [row.id, ...otherIds];
5987
+ }
5988
+ return [];
5989
+ };
5990
+ const migrateDocumentIdsWithLocalizations = async (db, knex2, meta) => {
5991
+ const singularName = meta.singularName.toLowerCase();
5992
+ const joinColumn = identifiers.getJoinColumnAttributeIdName(singularName);
5993
+ const inverseJoinColumn = identifiers.getInverseJoinColumnAttributeIdName(singularName);
5994
+ let ids;
5995
+ do {
5996
+ ids = await getNextIdsToCreateDocumentId(db, knex2, {
5997
+ joinColumn,
5998
+ inverseJoinColumn,
5999
+ tableName: meta.tableName,
6000
+ joinTableName: identifiers.getJoinTableName(meta.tableName, `localizations`)
6001
+ });
6002
+ if (ids.length > 0) {
6003
+ await knex2(meta.tableName).update({ document_id: createId() }).whereIn("id", ids);
6004
+ }
6005
+ } while (ids.length > 0);
6006
+ };
6007
+ const migrationDocumentIds = async (db, knex2, meta) => {
6008
+ let updatedRows;
6009
+ do {
6010
+ updatedRows = await knex2(meta.tableName).update({ document_id: createId() }).whereIn(
6011
+ "id",
6012
+ knex2(meta.tableName).select("id").from(knex2(meta.tableName).select("id").whereNull("document_id").limit(1).as("sub_query"))
6013
+ );
6014
+ } while (updatedRows > 0);
6015
+ };
6016
+ const createDocumentIdColumn = async (knex2, tableName) => {
6017
+ await knex2.schema.alterTable(tableName, (table) => {
6018
+ table.string("document_id");
6019
+ });
6020
+ };
6021
+ const hasLocalizationsJoinTable = async (knex2, tableName) => {
6022
+ const joinTableName = identifiers.getJoinTableName(tableName, "localizations");
6023
+ return knex2.schema.hasTable(joinTableName);
6024
+ };
6025
+ const createdDocumentId = {
6026
+ name: "5.0.0-02-created-document-id",
6027
+ async up(knex2, db) {
6028
+ for (const meta of db.metadata.values()) {
6029
+ const hasTable = await knex2.schema.hasTable(meta.tableName);
6030
+ if (!hasTable) {
6031
+ continue;
6032
+ }
6033
+ if ("documentId" in meta.attributes) {
6034
+ const hasDocumentIdColumn = await knex2.schema.hasColumn(meta.tableName, "document_id");
6035
+ if (hasDocumentIdColumn) {
6036
+ continue;
6037
+ }
6038
+ await createDocumentIdColumn(knex2, meta.tableName);
6039
+ if (await hasLocalizationsJoinTable(knex2, meta.tableName)) {
6040
+ await migrateDocumentIdsWithLocalizations(db, knex2, meta);
6041
+ } else {
6042
+ await migrationDocumentIds(db, knex2, meta);
6043
+ }
6044
+ }
6045
+ }
6046
+ },
6047
+ async down() {
6048
+ throw new Error("not implemented");
6049
+ }
6050
+ };
6051
+ const debug = createDebug("strapi::database::migration");
6052
+ const renameIdentifiersLongerThanMaxLength = {
6053
+ name: "5.0.0-rename-identifiers-longer-than-max-length",
6054
+ async up(knex2, db) {
6055
+ const md = db.metadata;
6056
+ const diffs = findDiffs(md);
6057
+ for (const indexDiff of diffs.indexes) {
6058
+ await renameIndex(knex2, db, indexDiff);
6059
+ }
6060
+ for (const columnDiff of diffs.columns) {
6061
+ const { full, short } = columnDiff;
6062
+ const tableName = full.tableName;
6063
+ const hasTable = await knex2.schema.hasTable(tableName);
6064
+ if (hasTable) {
6065
+ const hasColumn = await knex2.schema.hasColumn(tableName, full.columnName);
6066
+ if (hasColumn) {
6067
+ await knex2.schema.alterTable(tableName, async (table) => {
6068
+ debug(`renaming column ${full.columnName} to ${short.columnName}`);
6069
+ table.renameColumn(full.columnName, short.columnName);
6070
+ });
6071
+ }
6072
+ }
6073
+ }
6074
+ for (const tableDiff of diffs.tables) {
6075
+ const hasTable = await knex2.schema.hasTable(tableDiff.full.tableName);
6076
+ if (hasTable) {
6077
+ debug(`renaming table ${tableDiff.full.tableName} to ${tableDiff.short.tableName}`);
6078
+ await knex2.schema.renameTable(tableDiff.full.tableName, tableDiff.short.tableName);
6079
+ }
6080
+ }
6081
+ },
6082
+ async down() {
6083
+ throw new Error("not implemented");
6084
+ }
6085
+ };
6086
+ const renameIndex = async (knex2, db, diff) => {
6087
+ const client = db.config.connection.client;
6088
+ const short = diff.short;
6089
+ const full = diff.full;
6090
+ if (full.indexName === short.indexName) {
6091
+ debug(`not renaming index ${full.indexName} because name hasn't changed`);
6092
+ return;
6093
+ }
6094
+ if (short.indexName.includes("_lnk_") || full.indexName.includes("_lnk_") || short.indexName.endsWith("fk") || full.indexName.endsWith("fk")) {
6095
+ return;
6096
+ }
6097
+ debug(`renaming index from ${full.indexName} to ${short.indexName}`);
6098
+ try {
6099
+ await knex2.transaction(async (trx) => {
6100
+ if (client === "mysql" || client === "mariadb") {
6101
+ await knex2.raw(
6102
+ `ALTER TABLE \`${full.tableName}\` RENAME INDEX \`${full.indexName}\` TO \`${short.indexName}\``
6103
+ ).transacting(trx);
6104
+ } else if (client === "pg" || client === "postgres") {
6105
+ await knex2.raw(`ALTER INDEX "${full.indexName}" RENAME TO "${short.indexName}"`).transacting(trx);
6106
+ } else if (client === "sqlite" || client === "better") {
6107
+ } else {
6108
+ debug("No db client name matches, not creating index");
6109
+ }
6110
+ });
6111
+ } catch (err) {
6112
+ debug(`error creating index: ${JSON.stringify(err)}`);
6113
+ }
6114
+ };
6115
+ const findDiffs = (shortMap) => {
6116
+ const diffs = {
6117
+ tables: [],
6118
+ columns: [],
6119
+ indexes: []
6120
+ };
6121
+ const shortArr = Array.from(shortMap.entries());
6122
+ shortArr.forEach(([, shortObj], index2) => {
6123
+ const fullTableName = identifiers.getUnshortenedName(shortObj.tableName);
6124
+ if (!fullTableName) {
6125
+ throw new Error(`Missing full table name for ${shortObj.tableName}`);
6126
+ }
6127
+ if (shortObj.tableName !== fullTableName) {
6128
+ diffs.tables.push({
6129
+ full: {
6130
+ index: index2,
6131
+ key: "tableName",
6132
+ tableName: fullTableName
6133
+ },
6134
+ short: {
6135
+ index: index2,
6136
+ key: "tableName",
6137
+ tableName: shortObj.tableName
6138
+ }
6139
+ });
6140
+ }
6141
+ for (const attrKey in shortObj.attributes) {
6142
+ if (shortObj.attributes[attrKey].type === "relation") {
6143
+ continue;
6144
+ }
6145
+ const attr = shortObj.attributes[attrKey];
6146
+ const shortColumnName = attr.columnName;
6147
+ const longColumnName = identifiers.getUnshortenedName(shortColumnName);
6148
+ if (!shortColumnName || !longColumnName) {
6149
+ throw new Error(`missing column name(s) for attribute ${JSON.stringify(attr, null, 2)}`);
6150
+ }
6151
+ if (shortColumnName && longColumnName && shortColumnName !== longColumnName) {
6152
+ diffs.columns.push({
6153
+ short: {
6154
+ index: index2,
6155
+ tableName: fullTableName,
6156
+ // NOTE: this means that we must rename columns before tables
6157
+ key: `attributes.${attrKey}`,
6158
+ columnName: shortColumnName
6159
+ },
6160
+ full: {
6161
+ index: index2,
6162
+ tableName: fullTableName,
6163
+ key: `attributes.${attrKey}`,
6164
+ columnName: longColumnName
6165
+ }
6166
+ });
6167
+ }
6168
+ }
6169
+ for (const attrKey in shortObj.indexes) {
6170
+ const shortIndexName = shortObj.indexes[attrKey].name;
6171
+ const longIndexName = identifiers.getUnshortenedName(shortIndexName);
6172
+ if (!longIndexName) {
6173
+ throw new Error(`Missing full index name for ${shortIndexName}`);
6174
+ }
6175
+ if (shortIndexName && longIndexName && shortIndexName !== longIndexName) {
6176
+ diffs.indexes.push({
6177
+ short: {
6178
+ index: index2,
6179
+ tableName: fullTableName,
6180
+ // NOTE: this means that we must rename columns before tables
6181
+ key: `indexes.${attrKey}`,
6182
+ indexName: shortIndexName
6183
+ },
6184
+ full: {
6185
+ index: index2,
6186
+ tableName: fullTableName,
6187
+ key: `indexes.${attrKey}`,
6188
+ indexName: longIndexName
6189
+ }
6190
+ });
6191
+ }
6192
+ }
6193
+ });
6194
+ return diffs;
6195
+ };
6196
+ const createLocaleColumn = async (db, tableName) => {
6197
+ await db.schema.alterTable(tableName, (table) => {
6198
+ table.string("locale");
6199
+ });
6200
+ };
6201
+ const createdLocale = {
6202
+ name: "5.0.0-03-created-locale",
6203
+ async up(knex2, db) {
6204
+ for (const meta of db.metadata.values()) {
6205
+ const hasTable = await knex2.schema.hasTable(meta.tableName);
6206
+ if (!hasTable) {
6207
+ continue;
6208
+ }
6209
+ const uid = meta.uid;
6210
+ const model = strapi.getModel(uid);
6211
+ if (!model) {
6212
+ continue;
6213
+ }
6214
+ if (isNil(meta.attributes.locale)) {
6215
+ await createLocaleColumn(knex2, meta.tableName);
6216
+ }
6217
+ }
6218
+ },
6219
+ async down() {
6220
+ throw new Error("not implemented");
6221
+ }
6222
+ };
6223
+ const internalMigrations = [
6224
+ renameIdentifiersLongerThanMaxLength,
6225
+ createdDocumentId,
6226
+ createdLocale
6227
+ ];
5682
6228
  const createInternalMigrationProvider = (db) => {
5683
6229
  const context = { db };
6230
+ const migrations = [...internalMigrations];
5684
6231
  const umzugProvider = new Umzug({
5685
6232
  storage: createStorage({ db, tableName: "strapi_migrations_internal" }),
5686
6233
  logger: console,
5687
6234
  context,
5688
- migrations: internalMigrations.map((migration) => {
6235
+ migrations: () => migrations.map((migration) => {
5689
6236
  return {
5690
6237
  name: migration.name,
5691
6238
  up: wrapTransaction(context.db)(migration.up),
@@ -5694,6 +6241,9 @@ const createInternalMigrationProvider = (db) => {
5694
6241
  })
5695
6242
  });
5696
6243
  return {
6244
+ async register(migration) {
6245
+ migrations.push(migration);
6246
+ },
5697
6247
  async shouldRun() {
5698
6248
  const pendingMigrations = await umzugProvider.pending();
5699
6249
  return pendingMigrations.length > 0;
@@ -5707,8 +6257,13 @@ const createInternalMigrationProvider = (db) => {
5707
6257
  };
5708
6258
  };
5709
6259
  const createMigrationsProvider = (db) => {
5710
- const providers = [createUserMigrationProvider(db), createInternalMigrationProvider(db)];
6260
+ const userProvider = createUserMigrationProvider(db);
6261
+ const internalProvider = createInternalMigrationProvider(db);
6262
+ const providers = [userProvider, internalProvider];
5711
6263
  return {
6264
+ providers: {
6265
+ internal: internalProvider
6266
+ },
5712
6267
  async shouldRun() {
5713
6268
  const shouldRunResponses = await Promise.all(
5714
6269
  providers.map((provider) => provider.shouldRun())
@@ -5904,8 +6459,14 @@ const validateBidirectionalRelations = async (db) => {
5904
6459
  for (const { relation, invRelation } of invalidLinks) {
5905
6460
  const modelMetadata = db.metadata.get(invRelation.target);
5906
6461
  const invModelMetadata = db.metadata.get(relation.target);
5907
- const joinTableName = getJoinTableName(modelMetadata.tableName, invRelation.inversedBy);
5908
- const inverseJoinTableName = getJoinTableName(invModelMetadata.tableName, relation.inversedBy);
6462
+ const joinTableName = identifiers.getJoinTableName(
6463
+ snakeCase(modelMetadata.tableName),
6464
+ snakeCase(invRelation.inversedBy)
6465
+ );
6466
+ const inverseJoinTableName = identifiers.getJoinTableName(
6467
+ snakeCase(invModelMetadata.tableName),
6468
+ snakeCase(relation.inversedBy)
6469
+ );
5909
6470
  const joinTableEmpty = await isLinkTableEmpty(db, joinTableName);
5910
6471
  const inverseJoinTableEmpty = await isLinkTableEmpty(db, inverseJoinTableName);
5911
6472
  if (joinTableEmpty) {
@@ -5951,7 +6512,7 @@ class Database {
5951
6512
  };
5952
6513
  this.dialect = getDialect(this);
5953
6514
  this.dialect.configure();
5954
- this.metadata = createMetadata();
6515
+ this.metadata = createMetadata([]);
5955
6516
  this.connection = createConnection(this.config.connection, {
5956
6517
  pool: { afterCreate: afterCreate(this) }
5957
6518
  });
@@ -6028,11 +6589,9 @@ class Database {
6028
6589
  await this.connection.destroy();
6029
6590
  }
6030
6591
  }
6031
- const utils = { identifiers };
6032
6592
  export {
6033
6593
  Database,
6034
6594
  index as errors,
6035
- isKnexQuery,
6036
- utils
6595
+ isKnexQuery
6037
6596
  };
6038
6597
  //# sourceMappingURL=index.mjs.map