@fourlights/strapi-plugin-deep-populate 1.2.4 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,3 @@
1
- import { klona } from "klona/json";
2
1
  import require$$1 from "crypto";
3
2
  import require$$0$1 from "child_process";
4
3
  import require$$0$2 from "os";
@@ -11,16 +10,17 @@ import require$$0$6 from "stream";
11
10
  import require$$2$1 from "util";
12
11
  import require$$0$8 from "constants";
13
12
  import "node:stream";
13
+ import { klona } from "klona/json";
14
14
  import delve from "dlv";
15
15
  import { dset } from "dset/merge";
16
16
  const config = {
17
- default: ({ env: env2 }) => ({ cachePopulate: true, augmentPopulateStar: true }),
17
+ default: ({ env: env2 }) => ({ useCache: true, replaceWildcard: true }),
18
18
  validator: (config2) => {
19
19
  }
20
20
  };
21
21
  const schema$1 = {
22
22
  kind: "collectionType",
23
- collectionName: "caches",
23
+ collectionName: "populate_cache",
24
24
  info: {
25
25
  singularName: "cache",
26
26
  pluralName: "caches",
@@ -64,161 +64,6 @@ const schema$1 = {
64
64
  };
65
65
  const cache$1 = { schema: schema$1 };
66
66
  const contentTypes$1 = { cache: cache$1 };
67
- async function hasDeepPopulateCacheFullTextIndex(db, tableName, columnName) {
68
- const knex = db.connection;
69
- const client = db.dialect.client;
70
- if (client === "sqlite") {
71
- return (await db.dialect.schemaInspector.getTables()).includes(`${tableName}_fts`);
72
- }
73
- if (client === "mysql" || client === "mysql2") {
74
- return (await db.dialect.schemaInspector.getIndexes(tableName)).find(
75
- ({ name }) => name === `${tableName}_${columnName}_fulltext`
76
- ) !== void 0;
77
- }
78
- if (client === "pg" || client === "postgres") {
79
- const result = await knex.raw(
80
- `SELECT * FROM pg_indexes WHERE tablename = '${tableName}' AND indexname = '${tableName}_${columnName}_gin'`
81
- );
82
- return result.rows.length > 0;
83
- }
84
- console.log(`Full-text index not supported for this database engine (${client})`);
85
- return false;
86
- }
87
- async function addDeepPopulateCacheFullTextIndex(db, tableName, columnName) {
88
- const knex = db.connection;
89
- const hasTable = await knex.schema.hasTable(tableName);
90
- if (!hasTable) return;
91
- const hasColumn = await knex.schema.hasColumn(tableName, columnName);
92
- if (!hasColumn) return;
93
- const client = db.dialect.client;
94
- if (client === "sqlite") {
95
- await knex.raw(`CREATE VIRTUAL TABLE ${tableName}_fts USING fts3(${columnName})`);
96
- await knex.raw(`INSERT INTO ${tableName}_fts (${columnName}) SELECT ${columnName} FROM ${tableName}`);
97
- } else if (client === "mysql" || client === "mysql2") {
98
- await knex.raw(`ALTER TABLE ${tableName} ADD FULLTEXT INDEX ${tableName}_${columnName}_fulltext (${columnName})`);
99
- } else if (client === "pg" || client === "postgres") {
100
- await knex.raw(
101
- `CREATE INDEX ${tableName}_${columnName}_gin ON ${tableName} USING GIN (to_tsvector('english', ${columnName}))`
102
- );
103
- } else {
104
- console.log(`Full-text index not supported for this database engine (${client})`);
105
- }
106
- }
107
- async function removeDeepPopulateCacheFullTextIndex(db, tableName, columnName) {
108
- const knex = db.connection;
109
- const hasTable = await knex.schema.hasTable(tableName);
110
- if (!hasTable) return;
111
- const hasColumn = await knex.schema.hasColumn(tableName, columnName);
112
- if (!hasColumn) return;
113
- const client = db.dialect.client;
114
- if (client === "sqlite") {
115
- await knex.raw(`DROP TABLE ${tableName}_fts`);
116
- } else if (client === "mysql" || client === "mysql2") {
117
- await knex.raw(`ALTER TABLE ${tableName} DROP INDEX ${tableName}_${columnName}_fulltext`);
118
- } else if (client === "pg" || client === "postgres") {
119
- await knex.raw(`DROP INDEX ${tableName}_${columnName}_gin`);
120
- } else {
121
- console.log(`Full-text index not supported for this database engine (${client})`);
122
- }
123
- }
124
- const register = async ({ strapi: strapi2 }) => {
125
- strapi2.hook("strapi::content-types.afterSync").register(async () => {
126
- const tableName = "caches";
127
- const columnName = "dependencies";
128
- const hasIndex = await hasDeepPopulateCacheFullTextIndex(strapi2.db, tableName, columnName);
129
- const hasTable = await strapi2.db.connection.schema.hasTable(tableName);
130
- const hasColumn = hasTable && await strapi2.db.connection.schema.hasColumn(tableName, columnName);
131
- const cacheIsEnabled = strapi2.config.get("plugin::deep-populate").cachePopulate === true;
132
- const shouldCreateIndex = cacheIsEnabled && hasTable && hasColumn && !hasIndex;
133
- const shouldRemoveIndex = hasIndex && (!cacheIsEnabled || !hasTable || !hasColumn);
134
- if (shouldCreateIndex) await addDeepPopulateCacheFullTextIndex(strapi2.db, tableName, columnName);
135
- if (shouldRemoveIndex) await removeDeepPopulateCacheFullTextIndex(strapi2.db, tableName, columnName);
136
- });
137
- strapi2.documents.use(async (context, next) => {
138
- const { cachePopulate, augmentPopulateStar } = strapi2.config.get("plugin::deep-populate");
139
- if (
140
- // do nothing if not configured
141
- !cachePopulate && !augmentPopulateStar || context.uid === "plugin::deep-populate.cache"
142
- )
143
- return await next();
144
- const populateService = strapi2.plugin("deep-populate").service("populate");
145
- const cacheService = strapi2.plugin("deep-populate").service("cache");
146
- const { populate: populate2 } = context.params;
147
- const returnDeeplyPopulated = augmentPopulateStar && populate2 === "*";
148
- if (cachePopulate && context.action === "delete")
149
- await cacheService.clear({ ...context.params, contentType: context.uid });
150
- const originalFields = klona(context.fields);
151
- if (returnDeeplyPopulated && ["findOne", "findFirst", "findMany"].includes(context.action))
152
- context.fields = ["documentId", "status", "locale"];
153
- const result = await next();
154
- if (["create", "update"].includes(context.action)) {
155
- const { documentId, status: status2, locale: locale2 } = result;
156
- if (cachePopulate && context.action === "update")
157
- await cacheService.clear({ ...context.params, contentType: context.uid });
158
- if (cachePopulate || returnDeeplyPopulated) {
159
- const deepPopulate = await populateService.get({ contentType: context.uid, documentId, status: status2, locale: locale2 });
160
- if (returnDeeplyPopulated)
161
- return await strapi2.documents(context.uid).findOne({ documentId, status: status2, locale: locale2, fields: originalFields, populate: deepPopulate });
162
- }
163
- }
164
- if (returnDeeplyPopulated && ["findOne", "findFirst"].includes(context.action)) {
165
- const { documentId, status: status2, locale: locale2 } = result;
166
- const deepPopulate = await populateService.get({ contentType: context.uid, documentId, status: status2, locale: locale2 });
167
- return await strapi2.documents(context.uid).findOne({ documentId, status: status2, locale: locale2, fields: originalFields, populate: deepPopulate });
168
- }
169
- if (returnDeeplyPopulated && context.action === "findMany") {
170
- return await Promise.all(
171
- result.map(async ({ documentId, status: status2, locale: locale2 }) => {
172
- const deepPopulate = await populateService.get({ contentType: context.uid, documentId, status: status2, locale: locale2 });
173
- return await strapi2.documents(context.uid).findOne({ documentId, status: status2, locale: locale2, fields: originalFields, populate: deepPopulate });
174
- })
175
- );
176
- }
177
- return result;
178
- });
179
- };
180
- const getHash = (params) => {
181
- return `${params.contentType}-${params.documentId}-${params.locale}-${params.status}-${params.omitEmpty ? "sparse" : "full"}`;
182
- };
183
- const cache = ({ strapi: strapi2 }) => ({
184
- async get(params) {
185
- const entry = await strapi2.documents("plugin::deep-populate.cache").findFirst({ filters: { hash: { $eq: getHash(params) } } });
186
- return entry ? entry.populate : null;
187
- },
188
- async set({ populate: populate2, dependencies, ...params }) {
189
- const documentService = strapi2.documents("plugin::deep-populate.cache");
190
- const hash = getHash(params);
191
- const entry = await documentService.findFirst({ filters: { hash: { $eq: hash } } });
192
- return entry ? await documentService.update({
193
- documentId: entry.documentId,
194
- data: { populate: populate2, dependencies: dependencies.join(",") }
195
- }) : await documentService.create({ data: { hash, params, populate: populate2, dependencies: dependencies.join(",") } });
196
- },
197
- async clear(params) {
198
- const entry = await strapi2.documents("plugin::deep-populate.cache").findFirst({ filters: { hash: { $eq: getHash(params) } } });
199
- let retval = null;
200
- if (entry) {
201
- retval = await strapi2.documents("plugin::deep-populate.cache").delete({ documentId: entry.documentId });
202
- }
203
- await this.refreshDependents(params.documentId);
204
- return retval;
205
- },
206
- async refreshDependents(documentId) {
207
- const entries = await strapi2.documents("plugin::deep-populate.cache").findMany({ filters: { dependencies: { $contains: documentId } }, fields: ["documentId", "params"] });
208
- const deleted = await strapi2.db.query("plugin::deep-populate.cache").deleteMany({
209
- where: {
210
- documentId: { $in: entries.map((x) => x.documentId) }
211
- }
212
- });
213
- if (deleted.count !== entries.length)
214
- console.error(`Deleted count ${deleted.count} does not match entries count ${entries.length}`);
215
- const batchSize = 5;
216
- for (let i = 0; i < entries.length; i += batchSize) {
217
- const batch = entries.slice(i, i + batchSize);
218
- await Promise.all(batch.map((entry) => strapi2.service("plugin::deep-populate.populate").get(entry.params)));
219
- }
220
- }
221
- });
222
67
  var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
223
68
  function getDefaultExportFromCjs(x) {
224
69
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
@@ -24336,7 +24181,7 @@ const createVisitorUtils = ({ data }) => ({
24336
24181
  data[key] = value;
24337
24182
  }
24338
24183
  });
24339
- fp.curry(traverseEntity);
24184
+ const traverseEntity$1 = fp.curry(traverseEntity);
24340
24185
  const GROUP_OPERATORS = ["$and", "$or"];
24341
24186
  const WHERE_OPERATORS = [
24342
24187
  "$not",
@@ -24429,6 +24274,100 @@ const visitor$7 = ({ schema: schema2, key, attribute }, { remove: remove2 }) =>
24429
24274
  remove2(key);
24430
24275
  }
24431
24276
  };
24277
+ const VALID_RELATION_ORDERING_KEYS = {
24278
+ strict: fp.isBoolean
24279
+ };
24280
+ const ACTIONS_TO_VERIFY$1 = ["find"];
24281
+ const { CREATED_BY_ATTRIBUTE: CREATED_BY_ATTRIBUTE$1, UPDATED_BY_ATTRIBUTE: UPDATED_BY_ATTRIBUTE$1 } = constants$1;
24282
+ const removeRestrictedRelations = (auth) => async ({ data, key, attribute, schema: schema2 }, { remove: remove2, set: set2 }) => {
24283
+ if (!attribute) {
24284
+ return;
24285
+ }
24286
+ const isRelation = attribute.type === "relation";
24287
+ if (!isRelation) {
24288
+ return;
24289
+ }
24290
+ const handleMorphRelation = async () => {
24291
+ const elements = data[key];
24292
+ if ("connect" in elements || "set" in elements || "disconnect" in elements) {
24293
+ const newValue = {};
24294
+ const connect = await handleMorphElements(elements.connect || []);
24295
+ const relSet = await handleMorphElements(elements.set || []);
24296
+ const disconnect = await handleMorphElements(elements.disconnect || []);
24297
+ if (connect.length > 0) {
24298
+ newValue.connect = connect;
24299
+ }
24300
+ if (relSet.length > 0) {
24301
+ newValue.set = relSet;
24302
+ }
24303
+ if (disconnect.length > 0) {
24304
+ newValue.disconnect = disconnect;
24305
+ }
24306
+ if ("options" in elements && typeof elements.options === "object" && elements.options !== null) {
24307
+ const filteredOptions = {};
24308
+ Object.keys(elements.options).forEach((key2) => {
24309
+ const validator = VALID_RELATION_ORDERING_KEYS[key2];
24310
+ if (validator && validator(elements.options[key2])) {
24311
+ filteredOptions[key2] = elements.options[key2];
24312
+ }
24313
+ });
24314
+ newValue.options = filteredOptions;
24315
+ } else {
24316
+ newValue.options = {};
24317
+ }
24318
+ set2(key, newValue);
24319
+ } else {
24320
+ const newMorphValue = await handleMorphElements(elements);
24321
+ if (newMorphValue.length) {
24322
+ set2(key, newMorphValue);
24323
+ }
24324
+ }
24325
+ };
24326
+ const handleMorphElements = async (elements) => {
24327
+ const allowedElements = [];
24328
+ if (!fp.isArray(elements)) {
24329
+ return allowedElements;
24330
+ }
24331
+ for (const element of elements) {
24332
+ if (!fp.isObject(element) || !("__type" in element)) {
24333
+ continue;
24334
+ }
24335
+ const scopes = ACTIONS_TO_VERIFY$1.map((action) => `${element.__type}.${action}`);
24336
+ const isAllowed = await hasAccessToSomeScopes$1(scopes, auth);
24337
+ if (isAllowed) {
24338
+ allowedElements.push(element);
24339
+ }
24340
+ }
24341
+ return allowedElements;
24342
+ };
24343
+ const handleRegularRelation = async () => {
24344
+ const scopes = ACTIONS_TO_VERIFY$1.map((action) => `${attribute.target}.${action}`);
24345
+ const isAllowed = await hasAccessToSomeScopes$1(scopes, auth);
24346
+ if (!isAllowed) {
24347
+ remove2(key);
24348
+ }
24349
+ };
24350
+ const isCreatorRelation = [CREATED_BY_ATTRIBUTE$1, UPDATED_BY_ATTRIBUTE$1].includes(key);
24351
+ if (isMorphToRelationalAttribute(attribute)) {
24352
+ await handleMorphRelation();
24353
+ return;
24354
+ }
24355
+ if (isCreatorRelation && schema2.options?.populateCreatorFields) {
24356
+ return;
24357
+ }
24358
+ await handleRegularRelation();
24359
+ };
24360
+ const hasAccessToSomeScopes$1 = async (scopes, auth) => {
24361
+ for (const scope of scopes) {
24362
+ try {
24363
+ await strapi.auth.verify(auth, { scope });
24364
+ return true;
24365
+ } catch {
24366
+ continue;
24367
+ }
24368
+ }
24369
+ return false;
24370
+ };
24432
24371
  const visitor$6 = ({ key, attribute }, { remove: remove2 }) => {
24433
24372
  if (isMorphToRelationalAttribute(attribute)) {
24434
24373
  remove2(key);
@@ -24439,6 +24378,54 @@ const visitor$5 = ({ key, attribute }, { remove: remove2 }) => {
24439
24378
  remove2(key);
24440
24379
  }
24441
24380
  };
24381
+ const removeDisallowedFields = (allowedFields = null) => ({ key, path: { attribute: path2 } }, { remove: remove2 }) => {
24382
+ if (allowedFields === null) {
24383
+ return;
24384
+ }
24385
+ if (!(fp.isArray(allowedFields) && allowedFields.every(fp.isString))) {
24386
+ throw new TypeError(
24387
+ `Expected array of strings for allowedFields but got "${typeof allowedFields}"`
24388
+ );
24389
+ }
24390
+ if (fp.isNil(path2)) {
24391
+ return;
24392
+ }
24393
+ const containedPaths = getContainedPaths$1(path2);
24394
+ const isPathAllowed = allowedFields.some(
24395
+ (p) => containedPaths.includes(p) || p.startsWith(`${path2}.`)
24396
+ );
24397
+ if (isPathAllowed) {
24398
+ return;
24399
+ }
24400
+ remove2(key);
24401
+ };
24402
+ const getContainedPaths$1 = (path2) => {
24403
+ const parts = fp.toPath(path2);
24404
+ return parts.reduce((acc, value, index2, list) => {
24405
+ return [...acc, list.slice(0, index2 + 1).join(".")];
24406
+ }, []);
24407
+ };
24408
+ const removeRestrictedFields = (restrictedFields = null) => ({ key, path: { attribute: path2 } }, { remove: remove2 }) => {
24409
+ if (restrictedFields === null) {
24410
+ remove2(key);
24411
+ return;
24412
+ }
24413
+ if (!(fp.isArray(restrictedFields) && restrictedFields.every(fp.isString))) {
24414
+ throw new TypeError(
24415
+ `Expected array of strings for restrictedFields but got "${typeof restrictedFields}"`
24416
+ );
24417
+ }
24418
+ if (restrictedFields.includes(path2)) {
24419
+ remove2(key);
24420
+ return;
24421
+ }
24422
+ const isRestrictedNested = restrictedFields.some(
24423
+ (allowedPath) => path2?.toString().startsWith(`${allowedPath}.`)
24424
+ );
24425
+ if (isRestrictedNested) {
24426
+ remove2(key);
24427
+ }
24428
+ };
24442
24429
  const visitor$4 = ({ schema: schema2, key, value }, { set: set2 }) => {
24443
24430
  if (key === "" && value === "*") {
24444
24431
  const { attributes } = schema2;
@@ -24448,6 +24435,17 @@ const visitor$4 = ({ schema: schema2, key, value }, { set: set2 }) => {
24448
24435
  set2("", newPopulateQuery);
24449
24436
  }
24450
24437
  };
24438
+ const index$4 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
24439
+ __proto__: null,
24440
+ expandWildcardPopulate: visitor$4,
24441
+ removeDisallowedFields,
24442
+ removeDynamicZones: visitor$5,
24443
+ removeMorphToRelations: visitor$6,
24444
+ removePassword: visitor$8,
24445
+ removePrivate: visitor$7,
24446
+ removeRestrictedFields,
24447
+ removeRestrictedRelations
24448
+ }, Symbol.toStringTag, { value: "Module" }));
24451
24449
  const DEFAULT_PATH = { raw: null, attribute: null };
24452
24450
  const traverseFactory = () => {
24453
24451
  const state = {
@@ -25003,6 +25001,25 @@ const fields = traverseFactory().intercept(isStringArray, async (visitor2, optio
25003
25001
  }));
25004
25002
  const traverseQueryFields = fp.curry(fields.traverse);
25005
25003
  const { ID_ATTRIBUTE: ID_ATTRIBUTE$2, DOC_ID_ATTRIBUTE: DOC_ID_ATTRIBUTE$2 } = constants$1;
25004
+ const sanitizePasswords = (ctx) => async (entity) => {
25005
+ if (!ctx.schema) {
25006
+ throw new Error("Missing schema in sanitizePasswords");
25007
+ }
25008
+ return traverseEntity$1(visitor$8, ctx, entity);
25009
+ };
25010
+ const defaultSanitizeOutput = async (ctx, entity) => {
25011
+ if (!ctx.schema) {
25012
+ throw new Error("Missing schema in defaultSanitizeOutput");
25013
+ }
25014
+ return traverseEntity$1(
25015
+ (...args) => {
25016
+ visitor$8(...args);
25017
+ visitor$7(...args);
25018
+ },
25019
+ ctx,
25020
+ entity
25021
+ );
25022
+ };
25006
25023
  const defaultSanitizeFilters = fp.curry((ctx, filters2) => {
25007
25024
  if (!ctx.schema) {
25008
25025
  throw new Error("Missing schema in defaultSanitizeFilters");
@@ -25117,6 +25134,144 @@ const defaultSanitizePopulate = fp.curry((ctx, populate2) => {
25117
25134
  traverseQueryPopulate(visitor$7, ctx)
25118
25135
  )(populate2);
25119
25136
  });
25137
+ const sanitizers = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
25138
+ __proto__: null,
25139
+ defaultSanitizeFields,
25140
+ defaultSanitizeFilters,
25141
+ defaultSanitizeOutput,
25142
+ defaultSanitizePopulate,
25143
+ defaultSanitizeSort,
25144
+ sanitizePasswords
25145
+ }, Symbol.toStringTag, { value: "Module" }));
25146
+ const createAPISanitizers = (opts) => {
25147
+ const { getModel } = opts;
25148
+ const sanitizeInput = (data, schema2, { auth } = {}) => {
25149
+ if (!schema2) {
25150
+ throw new Error("Missing schema in sanitizeInput");
25151
+ }
25152
+ if (fp.isArray(data)) {
25153
+ return Promise.all(data.map((entry) => sanitizeInput(entry, schema2, { auth })));
25154
+ }
25155
+ const nonWritableAttributes = getNonWritableAttributes(schema2);
25156
+ const transforms = [
25157
+ // Remove first level ID in inputs
25158
+ fp.omit(constants$1.ID_ATTRIBUTE),
25159
+ fp.omit(constants$1.DOC_ID_ATTRIBUTE),
25160
+ // Remove non-writable attributes
25161
+ traverseEntity$1(removeRestrictedFields(nonWritableAttributes), { schema: schema2, getModel })
25162
+ ];
25163
+ if (auth) {
25164
+ transforms.push(
25165
+ traverseEntity$1(removeRestrictedRelations(auth), { schema: schema2, getModel })
25166
+ );
25167
+ }
25168
+ opts?.sanitizers?.input?.forEach((sanitizer) => transforms.push(sanitizer(schema2)));
25169
+ return pipe(...transforms)(data);
25170
+ };
25171
+ const sanitizeOutput = async (data, schema2, { auth } = {}) => {
25172
+ if (!schema2) {
25173
+ throw new Error("Missing schema in sanitizeOutput");
25174
+ }
25175
+ if (fp.isArray(data)) {
25176
+ const res = new Array(data.length);
25177
+ for (let i = 0; i < data.length; i += 1) {
25178
+ res[i] = await sanitizeOutput(data[i], schema2, { auth });
25179
+ }
25180
+ return res;
25181
+ }
25182
+ const transforms = [
25183
+ (data2) => defaultSanitizeOutput({ schema: schema2, getModel }, data2)
25184
+ ];
25185
+ if (auth) {
25186
+ transforms.push(
25187
+ traverseEntity$1(removeRestrictedRelations(auth), { schema: schema2, getModel })
25188
+ );
25189
+ }
25190
+ opts?.sanitizers?.output?.forEach((sanitizer) => transforms.push(sanitizer(schema2)));
25191
+ return pipe(...transforms)(data);
25192
+ };
25193
+ const sanitizeQuery = async (query, schema2, { auth } = {}) => {
25194
+ if (!schema2) {
25195
+ throw new Error("Missing schema in sanitizeQuery");
25196
+ }
25197
+ const { filters: filters2, sort: sort2, fields: fields2, populate: populate2 } = query;
25198
+ const sanitizedQuery = fp.cloneDeep(query);
25199
+ if (filters2) {
25200
+ Object.assign(sanitizedQuery, { filters: await sanitizeFilters(filters2, schema2, { auth }) });
25201
+ }
25202
+ if (sort2) {
25203
+ Object.assign(sanitizedQuery, { sort: await sanitizeSort(sort2, schema2, { auth }) });
25204
+ }
25205
+ if (fields2) {
25206
+ Object.assign(sanitizedQuery, { fields: await sanitizeFields(fields2, schema2) });
25207
+ }
25208
+ if (populate2) {
25209
+ Object.assign(sanitizedQuery, { populate: await sanitizePopulate(populate2, schema2) });
25210
+ }
25211
+ return sanitizedQuery;
25212
+ };
25213
+ const sanitizeFilters = (filters2, schema2, { auth } = {}) => {
25214
+ if (!schema2) {
25215
+ throw new Error("Missing schema in sanitizeFilters");
25216
+ }
25217
+ if (fp.isArray(filters2)) {
25218
+ return Promise.all(filters2.map((filter) => sanitizeFilters(filter, schema2, { auth })));
25219
+ }
25220
+ const transforms = [defaultSanitizeFilters({ schema: schema2, getModel })];
25221
+ if (auth) {
25222
+ transforms.push(
25223
+ traverseQueryFilters(removeRestrictedRelations(auth), { schema: schema2, getModel })
25224
+ );
25225
+ }
25226
+ return pipe(...transforms)(filters2);
25227
+ };
25228
+ const sanitizeSort = (sort2, schema2, { auth } = {}) => {
25229
+ if (!schema2) {
25230
+ throw new Error("Missing schema in sanitizeSort");
25231
+ }
25232
+ const transforms = [defaultSanitizeSort({ schema: schema2, getModel })];
25233
+ if (auth) {
25234
+ transforms.push(
25235
+ traverseQuerySort(removeRestrictedRelations(auth), { schema: schema2, getModel })
25236
+ );
25237
+ }
25238
+ return pipe(...transforms)(sort2);
25239
+ };
25240
+ const sanitizeFields = (fields2, schema2) => {
25241
+ if (!schema2) {
25242
+ throw new Error("Missing schema in sanitizeFields");
25243
+ }
25244
+ const transforms = [defaultSanitizeFields({ schema: schema2, getModel })];
25245
+ return pipe(...transforms)(fields2);
25246
+ };
25247
+ const sanitizePopulate = (populate2, schema2, { auth } = {}) => {
25248
+ if (!schema2) {
25249
+ throw new Error("Missing schema in sanitizePopulate");
25250
+ }
25251
+ const transforms = [defaultSanitizePopulate({ schema: schema2, getModel })];
25252
+ if (auth) {
25253
+ transforms.push(
25254
+ traverseQueryPopulate(removeRestrictedRelations(auth), { schema: schema2, getModel })
25255
+ );
25256
+ }
25257
+ return pipe(...transforms)(populate2);
25258
+ };
25259
+ return {
25260
+ input: sanitizeInput,
25261
+ output: sanitizeOutput,
25262
+ query: sanitizeQuery,
25263
+ filters: sanitizeFilters,
25264
+ sort: sanitizeSort,
25265
+ fields: sanitizeFields,
25266
+ populate: sanitizePopulate
25267
+ };
25268
+ };
25269
+ const index$2 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
25270
+ __proto__: null,
25271
+ createAPISanitizers,
25272
+ sanitizers,
25273
+ visitors: index$4
25274
+ }, Symbol.toStringTag, { value: "Module" }));
25120
25275
  [constants$1.DOC_ID_ATTRIBUTE, constants$1.DOC_ID_ATTRIBUTE];
25121
25276
  const isCamelCase = (value) => /^[a-z][a-zA-Z0-9]+$/.test(value);
25122
25277
  const isKebabCase = (value) => /^([a-z][a-z0-9]*)(-[a-z0-9]+)*$/.test(value);
@@ -25242,6 +25397,175 @@ setLocale({
25242
25397
  }
25243
25398
  }
25244
25399
  });
25400
+ async function hasDeepPopulateCacheFullTextIndex(db, tableName, columnName) {
25401
+ const knex = db.connection;
25402
+ const client = db.dialect.client;
25403
+ if (client === "sqlite") {
25404
+ return (await db.dialect.schemaInspector.getTables()).includes(`${tableName}_fts`);
25405
+ }
25406
+ if (client === "mysql" || client === "mysql2") {
25407
+ return (await db.dialect.schemaInspector.getIndexes(tableName)).find(
25408
+ ({ name }) => name === `${tableName}_${columnName}_fulltext`
25409
+ ) !== void 0;
25410
+ }
25411
+ if (client === "pg" || client === "postgres") {
25412
+ const result = await knex.raw(
25413
+ `SELECT * FROM pg_indexes WHERE tablename = '${tableName}' AND indexname = '${tableName}_${columnName}_gin'`
25414
+ );
25415
+ return result.rows.length > 0;
25416
+ }
25417
+ console.log(`Full-text index not supported for this database engine (${client})`);
25418
+ return false;
25419
+ }
25420
+ async function addDeepPopulateCacheFullTextIndex(db, tableName, columnName) {
25421
+ const knex = db.connection;
25422
+ const hasTable = await knex.schema.hasTable(tableName);
25423
+ if (!hasTable) return;
25424
+ const hasColumn = await knex.schema.hasColumn(tableName, columnName);
25425
+ if (!hasColumn) return;
25426
+ const client = db.dialect.client;
25427
+ if (client === "sqlite") {
25428
+ await knex.raw(`CREATE VIRTUAL TABLE ${tableName}_fts USING fts3(${columnName})`);
25429
+ await knex.raw(`INSERT INTO ${tableName}_fts (${columnName}) SELECT ${columnName} FROM ${tableName}`);
25430
+ } else if (client === "mysql" || client === "mysql2") {
25431
+ await knex.raw(`ALTER TABLE ${tableName} ADD FULLTEXT INDEX ${tableName}_${columnName}_fulltext (${columnName})`);
25432
+ } else if (client === "pg" || client === "postgres") {
25433
+ await knex.raw(
25434
+ `CREATE INDEX ${tableName}_${columnName}_gin ON ${tableName} USING GIN (to_tsvector('english', ${columnName}))`
25435
+ );
25436
+ } else {
25437
+ console.log(`Full-text index not supported for this database engine (${client})`);
25438
+ }
25439
+ }
25440
+ async function removeDeepPopulateCacheFullTextIndex(db, tableName, columnName) {
25441
+ const knex = db.connection;
25442
+ const hasTable = await knex.schema.hasTable(tableName);
25443
+ if (!hasTable) return;
25444
+ const hasColumn = await knex.schema.hasColumn(tableName, columnName);
25445
+ if (!hasColumn) return;
25446
+ const client = db.dialect.client;
25447
+ if (client === "sqlite") {
25448
+ await knex.raw(`DROP TABLE ${tableName}_fts`);
25449
+ } else if (client === "mysql" || client === "mysql2") {
25450
+ await knex.raw(`ALTER TABLE ${tableName} DROP INDEX ${tableName}_${columnName}_fulltext`);
25451
+ } else if (client === "pg" || client === "postgres") {
25452
+ await knex.raw(`DROP INDEX ${tableName}_${columnName}_gin`);
25453
+ } else {
25454
+ console.log(`Full-text index not supported for this database engine (${client})`);
25455
+ }
25456
+ }
25457
+ const populateIsWildcardEquivalent = async ({
25458
+ strapi: strapi2,
25459
+ schema: schema2,
25460
+ populate: populate2
25461
+ }) => {
25462
+ const expandedWildcardQuery = await index$2.sanitizers.defaultSanitizePopulate(
25463
+ {
25464
+ schema: schema2,
25465
+ getModel: (uid) => strapi2.getModel(uid)
25466
+ },
25467
+ "*"
25468
+ );
25469
+ return populate2 === "*" || populate2 === true || JSON.stringify(expandedWildcardQuery) === JSON.stringify(populate2);
25470
+ };
25471
+ const register = async ({ strapi: strapi2 }) => {
25472
+ strapi2.hook("strapi::content-types.afterSync").register(async () => {
25473
+ const tableName = "populate_cache";
25474
+ const columnName = "dependencies";
25475
+ const hasIndex = await hasDeepPopulateCacheFullTextIndex(strapi2.db, tableName, columnName);
25476
+ const hasTable = await strapi2.db.connection.schema.hasTable(tableName);
25477
+ const hasColumn = hasTable && await strapi2.db.connection.schema.hasColumn(tableName, columnName);
25478
+ const cacheIsEnabled = strapi2.config.get("plugin::deep-populate").useCache === true;
25479
+ const shouldCreateIndex = cacheIsEnabled && hasTable && hasColumn && !hasIndex;
25480
+ const shouldRemoveIndex = hasIndex && (!cacheIsEnabled || !hasTable || !hasColumn);
25481
+ if (shouldCreateIndex) await addDeepPopulateCacheFullTextIndex(strapi2.db, tableName, columnName);
25482
+ if (shouldRemoveIndex) await removeDeepPopulateCacheFullTextIndex(strapi2.db, tableName, columnName);
25483
+ });
25484
+ strapi2.documents.use(async (context, next) => {
25485
+ const { useCache, replaceWildcard } = strapi2.config.get("plugin::deep-populate");
25486
+ if (
25487
+ // do nothing if not configured
25488
+ !useCache && !replaceWildcard || context.uid === "plugin::deep-populate.cache"
25489
+ )
25490
+ return await next();
25491
+ const populateService = strapi2.plugin("deep-populate").service("populate");
25492
+ const cacheService = strapi2.plugin("deep-populate").service("cache");
25493
+ const { populate: populate2 } = context.params;
25494
+ const returnDeeplyPopulated = replaceWildcard && await populateIsWildcardEquivalent({ strapi: strapi2, schema: context.contentType, populate: populate2 });
25495
+ if (useCache && context.action === "delete")
25496
+ await cacheService.clear({ ...context.params, contentType: context.uid });
25497
+ const originalFields = klona(context.fields);
25498
+ if (returnDeeplyPopulated && ["findOne", "findFirst", "findMany"].includes(context.action))
25499
+ context.fields = ["documentId", "status", "locale"];
25500
+ const result = await next();
25501
+ if (["create", "update"].includes(context.action)) {
25502
+ const { documentId, status: status2, locale: locale2 } = result;
25503
+ if (useCache && context.action === "update")
25504
+ await cacheService.clear({ ...context.params, contentType: context.uid });
25505
+ if (useCache || returnDeeplyPopulated) {
25506
+ const deepPopulate = await populateService.get({ contentType: context.uid, documentId, status: status2, locale: locale2 });
25507
+ if (returnDeeplyPopulated)
25508
+ return await strapi2.documents(context.uid).findOne({ documentId, status: status2, locale: locale2, fields: originalFields, populate: deepPopulate });
25509
+ }
25510
+ }
25511
+ if (returnDeeplyPopulated && ["findOne", "findFirst"].includes(context.action)) {
25512
+ const { documentId, status: status2, locale: locale2 } = result;
25513
+ const deepPopulate = await populateService.get({ contentType: context.uid, documentId, status: status2, locale: locale2 });
25514
+ return await strapi2.documents(context.uid).findOne({ documentId, status: status2, locale: locale2, fields: originalFields, populate: deepPopulate });
25515
+ }
25516
+ if (returnDeeplyPopulated && context.action === "findMany") {
25517
+ return await Promise.all(
25518
+ result.map(async ({ documentId, status: status2, locale: locale2 }) => {
25519
+ const deepPopulate = await populateService.get({ contentType: context.uid, documentId, status: status2, locale: locale2 });
25520
+ return await strapi2.documents(context.uid).findOne({ documentId, status: status2, locale: locale2, fields: originalFields, populate: deepPopulate });
25521
+ })
25522
+ );
25523
+ }
25524
+ return result;
25525
+ });
25526
+ };
25527
+ const getHash = (params) => {
25528
+ return `${params.contentType}-${params.documentId}-${params.locale}-${params.status}-${params.omitEmpty ? "sparse" : "full"}`;
25529
+ };
25530
+ const cache = ({ strapi: strapi2 }) => ({
25531
+ async get(params) {
25532
+ const entry = await strapi2.documents("plugin::deep-populate.cache").findFirst({ filters: { hash: { $eq: getHash(params) } } });
25533
+ return entry ? entry.populate : null;
25534
+ },
25535
+ async set({ populate: populate2, dependencies, ...params }) {
25536
+ const documentService = strapi2.documents("plugin::deep-populate.cache");
25537
+ const hash = getHash(params);
25538
+ const entry = await documentService.findFirst({ filters: { hash: { $eq: hash } } });
25539
+ return entry ? await documentService.update({
25540
+ documentId: entry.documentId,
25541
+ data: { populate: populate2, dependencies: dependencies.join(",") }
25542
+ }) : await documentService.create({ data: { hash, params, populate: populate2, dependencies: dependencies.join(",") } });
25543
+ },
25544
+ async clear(params) {
25545
+ const entry = await strapi2.documents("plugin::deep-populate.cache").findFirst({ filters: { hash: { $eq: getHash(params) } } });
25546
+ let retval = null;
25547
+ if (entry) {
25548
+ retval = await strapi2.documents("plugin::deep-populate.cache").delete({ documentId: entry.documentId });
25549
+ }
25550
+ await this.refreshDependents(params.documentId);
25551
+ return retval;
25552
+ },
25553
+ async refreshDependents(documentId) {
25554
+ const entries = await strapi2.documents("plugin::deep-populate.cache").findMany({ filters: { dependencies: { $contains: documentId } }, fields: ["documentId", "params"] });
25555
+ const deleted = await strapi2.db.query("plugin::deep-populate.cache").deleteMany({
25556
+ where: {
25557
+ documentId: { $in: entries.map((x) => x.documentId) }
25558
+ }
25559
+ });
25560
+ if (deleted.count !== entries.length)
25561
+ console.error(`Deleted count ${deleted.count} does not match entries count ${entries.length}`);
25562
+ const batchSize = 5;
25563
+ for (let i = 0; i < entries.length; i += batchSize) {
25564
+ const batch = entries.slice(i, i + batchSize);
25565
+ await Promise.all(batch.map((entry) => strapi2.service("plugin::deep-populate.populate").get(entry.params)));
25566
+ }
25567
+ }
25568
+ });
25245
25569
  const getRelations = (model) => {
25246
25570
  const filteredAttributes = /* @__PURE__ */ new Set();
25247
25571
  const { populateCreatorFields } = contentTypes.getOptions(model);
@@ -25452,8 +25776,8 @@ async function populate$1(params) {
25452
25776
  }
25453
25777
  const populate = ({ strapi: strapi2 }) => ({
25454
25778
  async get(params) {
25455
- const { cachePopulate } = strapi2.config.get("plugin::deep-populate");
25456
- if (!cachePopulate) return (await populate$1(params)).populate;
25779
+ const { useCache } = strapi2.config.get("plugin::deep-populate");
25780
+ if (!useCache) return (await populate$1(params)).populate;
25457
25781
  const cachedEntry = await strapi2.service("plugin::deep-populate.cache").get(params);
25458
25782
  if (cachedEntry) return cachedEntry;
25459
25783
  const resolved = await populate$1(params);