adminizer 5.0.0-build.9 → 5.1.0-build.25

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 (232) hide show
  1. package/assets/abnfDiagram-VCTEODGH-BTgLIXrb.js +1 -0
  2. package/assets/add-C8V8qjvm.js +1 -0
  3. package/assets/add-group-Ezq_Ocot.js +1 -0
  4. package/assets/add-user-CluNf2QG.js +1 -0
  5. package/assets/ai-assistant/agent.es.js +28464 -0
  6. package/assets/app-C96pcs-J.js +138 -0
  7. package/assets/app-DPmI-MvN.css +1 -0
  8. package/assets/arc-G6n0TtRf.js +1 -0
  9. package/assets/architectureDiagram-5GKGNRK7-TQ4_iSAG.js +36 -0
  10. package/assets/asmcrypto-DZeQH0LC.js +1 -0
  11. package/assets/blockDiagram-I7D4REHJ-1q050zQd.js +129 -0
  12. package/assets/c4Diagram-7LVT6UL2-BXn0hW62.js +38 -0
  13. package/assets/{catalog-dk6a0I1u.js → catalog-Cpx4OH_r.js} +3 -3
  14. package/assets/channel-JZzXP_E5.js +1 -0
  15. package/assets/chunk-2Q5K7J3B-BSnocdBz.js +1 -0
  16. package/assets/chunk-5VM5RSS4-BvfoMSpq.js +15 -0
  17. package/assets/chunk-F27PBJKO-CMyFcBlX.js +1 -0
  18. package/assets/chunk-IMKFNOWR--8Y02hwt.js +231 -0
  19. package/assets/chunk-JWPE2WC7-C18m6v8d.js +1 -0
  20. package/assets/chunk-POPQ4Y6H-DzoTZe0a.js +1 -0
  21. package/assets/chunk-SVP7TREG-Ce98IXh7.js +88 -0
  22. package/assets/chunk-TICWLB2K-BeLoUlaL.js +206 -0
  23. package/assets/chunk-XXDRQBXY-DCZSZ1aW.js +1 -0
  24. package/assets/classDiagram-ZZMXUADV-jA4zeKxs.js +1 -0
  25. package/assets/classDiagram-v2-VYDZK3BY-jA4zeKxs.js +1 -0
  26. package/assets/controls/ckeditor.es.js +9382 -9320
  27. package/assets/controls/handsontable.es.js +11971 -11938
  28. package/assets/controls/jsoneditor.es.js +10442 -10233
  29. package/assets/controls/toast-ui.es.js +1308 -1279
  30. package/assets/cose-bilkent-JH36ORCC-Cy9np608.js +1 -0
  31. package/assets/cynefin-OW5HDTMX-Ba6uP79R.js +166 -0
  32. package/assets/cynefinDiagram-5FMLGOSQ-DhMBBq7T.js +62 -0
  33. package/assets/cytoscape.esm-DzSz-X2X.js +321 -0
  34. package/assets/dagre-GXQ25YYZ-BdAnQUCV.js +4 -0
  35. package/assets/dashboard-BFVFBIio.js +3 -0
  36. package/assets/debounce-B3mXI_sx.js +1 -0
  37. package/assets/defaultLocale-DX6XiGOO.js +1 -0
  38. package/assets/diagram-S7CK7UJ4-BDYUGbNl.js +30 -0
  39. package/assets/diagram-UQ7AKVKN-PURK0-X-.js +41 -0
  40. package/assets/diagram-VSXAHHWV-DxHdFxl9.js +3 -0
  41. package/assets/diagram-VX7I27RA-Vz8xmeYc.js +24 -0
  42. package/assets/diagram-Z3DM3KII-CFXUpBMy.js +24 -0
  43. package/assets/docs-DU3h-HtS.js +1 -0
  44. package/assets/ebnfDiagram-PWID7BFC-BXNvxMGP.js +1 -0
  45. package/assets/erDiagram-RLTQ6QDP-DN0GgZEI.js +99 -0
  46. package/assets/flowDiagram-HODETNUW-DXdnwgBv.js +168 -0
  47. package/assets/ganttDiagram-EL5Y4UJY-Cv8_dxX3.js +292 -0
  48. package/assets/gitGraphDiagram-WWUBYQGX-BO8T858E.js +106 -0
  49. package/assets/history-BmwWpckq.js +1 -0
  50. package/assets/infoDiagram-27XIBGKW-DMSJ1RNc.js +2 -0
  51. package/assets/init-Gi6I4Gst.js +1 -0
  52. package/assets/init-user-BIdB8U_4.js +1 -0
  53. package/assets/isObject-DL-BuaBl.js +1 -0
  54. package/assets/ishikawaDiagram-5VMMS53U-_tLJESbl.js +70 -0
  55. package/assets/journeyDiagram-3NMN7TZE-DMMhdo-_.js +139 -0
  56. package/assets/kanban-definition-UXKFOSKX-CxQrB4t6.js +89 -0
  57. package/assets/katex-HP8lGamR.js +257 -0
  58. package/assets/layout-_oS_xxCJ.js +1 -0
  59. package/assets/linear-VLYuNK3x.js +1 -0
  60. package/assets/{list-CcMtS4hD.js → list-YTyJQtiR.js} +2 -2
  61. package/assets/login-B9GlhjZh.js +1 -0
  62. package/assets/manifest.json +834 -58
  63. package/assets/media-manager-Bb1VWlzd.js +6 -0
  64. package/assets/mermaid.core-BknV2vft.js +310 -0
  65. package/assets/mindmap-definition-YA3MSWOX-DBjjjDq4.js +96 -0
  66. package/assets/module-DQ1NK8Xg.js +1 -0
  67. package/assets/notification-CgNOsXvn.js +20 -0
  68. package/assets/ordinal-Cboi1Yqb.js +1 -0
  69. package/assets/pegDiagram-XKGWAZYB-uRznTaO4.js +1 -0
  70. package/assets/pieDiagram-E7YTZNPT-D1TkCH8I.js +39 -0
  71. package/assets/quadrantDiagram-AXDQQJYC-BofJ36Jz.js +7 -0
  72. package/assets/railroadDiagram-O6MQD6OU-e1ZM2IT1.js +1 -0
  73. package/assets/register-DjTtMayF.js +1 -0
  74. package/assets/requirementDiagram-BXWQKSXE-DQWArRbb.js +84 -0
  75. package/assets/ru-CqAtka69.js +1 -0
  76. package/assets/sankeyDiagram-P5KCCOFB-fVOu2jzi.js +40 -0
  77. package/assets/sequenceDiagram-WJ2MYXX4-Dj24cqpu.js +162 -0
  78. package/assets/sizeCapture-INFHLROL-CsApVrVi.js +1 -0
  79. package/assets/stateDiagram-D77RDMKH-IU2Szlid.js +1 -0
  80. package/assets/stateDiagram-v2-MP3YSRHH-B-EW3cIC.js +1 -0
  81. package/assets/swimlanes-42K2YHIH-97rBqTR-.js +1 -0
  82. package/assets/swimlanesDiagram-VR7AAH4N-CqG2iydK.js +8 -0
  83. package/assets/timeline-definition-24CTP7MA-DSgRzx1V.js +120 -0
  84. package/assets/toNumber-DZb-r9sy.js +1 -0
  85. package/assets/use-appearance-CXz2xWgT.js +1 -0
  86. package/assets/user-filters-list-BAR4CBN_.js +1 -0
  87. package/assets/vennDiagram-4TSXK5OY-BqMksFxE.js +34 -0
  88. package/assets/wardleyDiagram-VM6X3IG4-BMmxRODC.js +78 -0
  89. package/assets/welcome-CPXT7_4b.js +1 -0
  90. package/assets/with-app-layout-C6dqetfx.js +31 -0
  91. package/assets/xychartDiagram-S5SC5T6Z-3xRuF0l4.js +7 -0
  92. package/controllers/access-rights/permissionOptions.d.ts +2 -0
  93. package/controllers/access-rights/permissionOptions.js +23 -0
  94. package/controllers/add.js +2 -2
  95. package/controllers/addGroup.js +15 -6
  96. package/controllers/ai/AiAgentController.d.ts +66 -0
  97. package/controllers/ai/AiAgentController.js +435 -0
  98. package/controllers/ai/AiAssistantController.d.ts +0 -4
  99. package/controllers/ai/AiAssistantController.js +4 -78
  100. package/controllers/catalog/FrontendCatalogAdapter.js +7 -8
  101. package/controllers/docs/index.d.ts +22 -0
  102. package/controllers/docs/index.js +126 -0
  103. package/controllers/edit.js +2 -2
  104. package/controllers/editGroup.js +15 -6
  105. package/controllers/filter-fields/savedFilters.js +1 -1
  106. package/controllers/history-actions/HistoryController.js +7 -7
  107. package/controllers/inlineUpdate.js +10 -3
  108. package/controllers/list.js +1 -1
  109. package/controllers/login.js +1 -1
  110. package/controllers/notifications/NotificationController.js +14 -8
  111. package/controllers/view.js +3 -3
  112. package/controllers/widgets/filterInfo.js +1 -1
  113. package/helpers/accessRightsHelper.d.ts +63 -2
  114. package/helpers/accessRightsHelper.js +202 -13
  115. package/helpers/assetVersionHelper.d.ts +14 -0
  116. package/helpers/assetVersionHelper.js +42 -0
  117. package/helpers/configHelper.js +19 -18
  118. package/helpers/controllerHelper.d.ts +1 -0
  119. package/helpers/controllerHelper.js +25 -15
  120. package/helpers/fieldsHelper.d.ts +2 -1
  121. package/helpers/fieldsHelper.js +11 -5
  122. package/helpers/inertiaActionsHelper.d.ts +1 -1
  123. package/helpers/inertiaActionsHelper.js +4 -5
  124. package/helpers/inertiaAddHelper.d.ts +1 -1
  125. package/helpers/inertiaAddHelper.js +7 -6
  126. package/helpers/inertiaGroupHelper.js +10 -2
  127. package/helpers/inertiaListHelper.d.ts +1 -1
  128. package/helpers/inertiaListHelper.js +7 -8
  129. package/helpers/inertiaMenuHelper.d.ts +7 -2
  130. package/helpers/inertiaMenuHelper.js +12 -27
  131. package/helpers/menuHelper.d.ts +11 -1
  132. package/helpers/menuHelper.js +12 -0
  133. package/helpers/modelResourceHelper.d.ts +9 -0
  134. package/helpers/modelResourceHelper.js +43 -0
  135. package/helpers/navigationAccessHelper.d.ts +13 -0
  136. package/helpers/navigationAccessHelper.js +45 -0
  137. package/index.d.ts +11 -0
  138. package/index.js +11 -0
  139. package/interfaces/adminpanelConfig.d.ts +160 -2
  140. package/interfaces/queryCriteria.d.ts +13 -0
  141. package/interfaces/types.d.ts +169 -0
  142. package/lib/Adminizer.d.ts +8 -0
  143. package/lib/Adminizer.js +30 -2
  144. package/lib/DataAccessor.d.ts +100 -3
  145. package/lib/DataAccessor.js +454 -245
  146. package/lib/access-graph/AccessGraphResolver.d.ts +136 -0
  147. package/lib/access-graph/AccessGraphResolver.js +605 -0
  148. package/lib/access-graph/RecordAccessCache.d.ts +23 -0
  149. package/lib/access-graph/RecordAccessCache.js +30 -0
  150. package/lib/access-graph/RecordAccessResolver.d.ts +83 -0
  151. package/lib/access-graph/RecordAccessResolver.js +295 -0
  152. package/lib/access-graph/shared.d.ts +60 -0
  153. package/lib/access-graph/shared.js +121 -0
  154. package/lib/admin-links/AdminLinkHandler.d.ts +72 -0
  155. package/lib/admin-links/AdminLinkHandler.js +183 -0
  156. package/lib/ai-assistant/AbstractAiConversationHistoryService.d.ts +39 -0
  157. package/lib/ai-assistant/AbstractAiConversationHistoryService.js +98 -0
  158. package/lib/ai-assistant/AbstractAiModelService.d.ts +139 -2
  159. package/lib/ai-assistant/AbstractAiModelService.js +154 -1
  160. package/lib/ai-assistant/AiAgentRunManager.d.ts +35 -0
  161. package/lib/ai-assistant/AiAgentRunManager.js +109 -0
  162. package/lib/ai-assistant/AiAssistantAgentSkillHandler.d.ts +61 -0
  163. package/lib/ai-assistant/AiAssistantAgentSkillHandler.js +90 -0
  164. package/lib/ai-assistant/AiAssistantHandler.d.ts +1 -0
  165. package/lib/ai-assistant/AiAssistantHandler.js +4 -0
  166. package/lib/ai-assistant/AiAssistantUiMethodHandler.d.ts +60 -0
  167. package/lib/ai-assistant/AiAssistantUiMethodHandler.js +144 -0
  168. package/lib/ai-assistant/builtinAgentSkills.d.ts +9 -0
  169. package/lib/ai-assistant/builtinAgentSkills.js +259 -0
  170. package/lib/ai-assistant/jsonSafe.d.ts +13 -0
  171. package/lib/ai-assistant/jsonSafe.js +81 -0
  172. package/lib/app-manager/AdminizerApp.d.ts +40 -1
  173. package/lib/app-manager/AppManager.js +48 -42
  174. package/lib/app-manager/ConfigLayerHandler.js +4 -0
  175. package/lib/docs/AbstractDocumentation.d.ts +100 -0
  176. package/lib/docs/AbstractDocumentation.js +108 -0
  177. package/lib/docs/DocumentationHandler.d.ts +100 -0
  178. package/lib/docs/DocumentationHandler.js +198 -0
  179. package/lib/docs/FileDocumentation.d.ts +47 -0
  180. package/lib/docs/FileDocumentation.js +189 -0
  181. package/lib/docs/docsAgentSkills.d.ts +10 -0
  182. package/lib/docs/docsAgentSkills.js +93 -0
  183. package/lib/filters/FilterService.js +4 -3
  184. package/lib/history-actions/AbstractHistoryAdapter.d.ts +26 -1
  185. package/lib/history-actions/AbstractHistoryAdapter.js +155 -25
  186. package/lib/media-manager/helpers/MediaManagerHelper.js +9 -7
  187. package/lib/model/AbstractModel.d.ts +48 -0
  188. package/lib/model/AbstractModel.js +14 -4
  189. package/lib/model/ModelHandler.d.ts +61 -2
  190. package/lib/model/ModelHandler.js +136 -8
  191. package/lib/model/adapter/sequelize.d.ts +40 -1
  192. package/lib/model/adapter/sequelize.js +243 -77
  193. package/lib/model/adapter/typeorm.d.ts +16 -0
  194. package/lib/model/adapter/typeorm.js +136 -9
  195. package/lib/notifications/GeneralNotificationService.js +6 -5
  196. package/lib/notifications/SystemNotificationService.js +1 -1
  197. package/lib/ui-i18n/uiTranslationKeys.d.ts +2 -1
  198. package/lib/ui-i18n/uiTranslationKeys.js +42 -1
  199. package/lib/widgets/widgetHandler.js +10 -6
  200. package/models/Group.d.ts +2 -1
  201. package/package.json +35 -23
  202. package/policies/authPolicies.js +6 -6
  203. package/policies/permissionResolvers.d.ts +1 -0
  204. package/policies/permissionResolvers.js +3 -0
  205. package/system/Router.js +21 -3
  206. package/system/bindAccessRights.js +13 -3
  207. package/system/bindDocs.d.ts +13 -0
  208. package/system/bindDocs.js +77 -0
  209. package/system/bindInertia.js +30 -7
  210. package/system/buildInternalModelAccess.d.ts +9 -0
  211. package/system/buildInternalModelAccess.js +37 -4
  212. package/system/defaults.js +7 -1
  213. package/system/validateSystemModels.js +29 -9
  214. package/assets/add-CBlLpLr0.js +0 -1
  215. package/assets/add-group-H0IKIsNm.js +0 -1
  216. package/assets/add-user-CoTLmxtF.js +0 -1
  217. package/assets/app-DNXiApCk.css +0 -1
  218. package/assets/app-layout-xxJpLAvb.js +0 -1
  219. package/assets/app.js +0 -138
  220. package/assets/dashboard-DVJS5EPP.js +0 -3
  221. package/assets/debounce-BMmXVQ06.js +0 -1
  222. package/assets/history-bWRuzbye.js +0 -1
  223. package/assets/init-user-8aUohVDq.js +0 -1
  224. package/assets/login-CO4laf6m.js +0 -1
  225. package/assets/media-manager-BDbpSSjH.js +0 -6
  226. package/assets/module-CqO_BKsH.js +0 -1
  227. package/assets/notification-B5l2wwmi.js +0 -20
  228. package/assets/register-DBYIMXGQ.js +0 -1
  229. package/assets/ru-rYKdQbWF.js +0 -1
  230. package/assets/use-appearance-_EbbjHTh.js +0 -1
  231. package/assets/user-filters-list-C7kMKN5f.js +0 -1
  232. package/assets/welcome-6z-7sXnQ.js +0 -1
@@ -1,4 +1,5 @@
1
1
  import { AbstractAdapter, AbstractModel } from "../AbstractModel.js";
2
+ import { Adminizer } from "../../Adminizer.js";
2
3
  let _Sequelize = null;
3
4
  let _Op = null;
4
5
  let _HasMany = null;
@@ -116,6 +117,113 @@ export class SequelizeModel extends AbstractModel {
116
117
  super(modelName, mapSequelizeToAdminizerAttributes(model), model.primaryKeyAttribute, model.name);
117
118
  this.model = model;
118
119
  }
120
+ /**
121
+ * accessGraph pushdown: compiles the level chain into one nested-subquery literal for
122
+ * an `{in: ...}` criteria, so the DB planner optimizes the whole walk instead of the
123
+ * app materializing id lists. Returns undefined — falling back to materialization —
124
+ * when any level lives on another adapter or connection, an attribute is unknown, or a
125
+ * level carries a defaultScope we cannot reproduce.
126
+ *
127
+ * The materialized path queries through `model.find()`, so it inherits paranoid and
128
+ * defaultScope filtering; this raw SQL must match it exactly or the same config would
129
+ * yield two different record sets depending on `pushdown`.
130
+ *
131
+ * Every bail-out reports through `decline` before returning: a fallback is invisible in the
132
+ * result (the records are the same, only slower), so the reason is the only thing that tells
133
+ * an operator why `pushdown: true` changed nothing.
134
+ */
135
+ async compileGraphInSubquery(levels, decline) {
136
+ await this._ensure();
137
+ /** Reports the reason and bails out of the whole compilation. */
138
+ const no = (reason) => {
139
+ decline?.(reason);
140
+ return undefined;
141
+ };
142
+ const targets = [];
143
+ for (const level of levels) {
144
+ if (!(level.model instanceof SequelizeModel)) {
145
+ return no(`"${level.model.modelname}" is not a Sequelize model`);
146
+ }
147
+ const target = level.model.model;
148
+ if (!target.sequelize || target.sequelize !== this.model.sequelize) {
149
+ return no(`"${level.model.modelname}" lives on another Sequelize connection`);
150
+ }
151
+ targets.push(target);
152
+ }
153
+ const queryGenerator = this.model.sequelize.getQueryInterface().queryGenerator;
154
+ // Adminizer attribute (association alias or plain column) → quoted physical column
155
+ const columnFor = (levelModel, target, attributeName) => {
156
+ const attribute = levelModel.attributes?.[attributeName];
157
+ const rawName = (attribute?.type === "association" && attribute.via) ? attribute.via : attributeName;
158
+ const raw = target.rawAttributes?.[rawName];
159
+ if (!raw) {
160
+ return undefined;
161
+ }
162
+ return queryGenerator.quoteIdentifier(raw.field ?? rawName);
163
+ };
164
+ let sql;
165
+ for (let i = 0; i < levels.length; i++) {
166
+ const level = levels[i];
167
+ const target = targets[i];
168
+ const table = queryGenerator.quoteTable(target.getTableName());
169
+ const selectColumn = columnFor(level.model, target, level.select);
170
+ if (!selectColumn) {
171
+ return no(`"${level.model.modelname}.${level.select}" has no column to select`);
172
+ }
173
+ const modelOptions = target.options ?? {};
174
+ // A defaultScope silently narrows every findAll on the model; reproducing an
175
+ // arbitrary one in raw SQL is not possible, so this chain is not pushed down.
176
+ if (modelOptions.defaultScope && Object.keys(modelOptions.defaultScope).length) {
177
+ return no(`"${level.model.modelname}" declares a defaultScope, which raw SQL cannot reproduce `
178
+ + `(pushing down would return a different record set than the materialized walk)`);
179
+ }
180
+ // Conditions that constrain the level (parent link / literal ids) versus guards that
181
+ // only exclude rows (paranoid). A level constrained by guards alone is NOT narrowed,
182
+ // so the emptiness check below must look at linkConditions only.
183
+ const linkConditions = [];
184
+ const guardConditions = [];
185
+ if (modelOptions.paranoid) {
186
+ const deletedAtAttribute = target._timestampAttributes?.deletedAt
187
+ ?? modelOptions.deletedAt
188
+ ?? "deletedAt";
189
+ const rawDeletedAt = target.rawAttributes?.[deletedAtAttribute];
190
+ if (!rawDeletedAt) {
191
+ return no(`"${level.model.modelname}" is paranoid but its "${deletedAtAttribute}" column cannot be `
192
+ + `resolved, so soft-deleted rows could not be excluded`);
193
+ }
194
+ const deletedAtColumn = queryGenerator.quoteIdentifier(rawDeletedAt.field ?? deletedAtAttribute);
195
+ guardConditions.push(`${table}.${deletedAtColumn} IS NULL`);
196
+ }
197
+ if (level.parentAttribute) {
198
+ if (!sql) {
199
+ return no(`"${level.model.modelname}" links to an inner level that produced no subquery`);
200
+ }
201
+ const parentColumn = columnFor(level.model, target, level.parentAttribute);
202
+ if (!parentColumn) {
203
+ return no(`"${level.model.modelname}.${level.parentAttribute}" has no column to link on`);
204
+ }
205
+ linkConditions.push(`${table}.${parentColumn} IN ${sql}`);
206
+ }
207
+ for (const condition of level.conditions ?? []) {
208
+ const column = columnFor(level.model, target, condition.attribute);
209
+ if (!column) {
210
+ return no(`"${level.model.modelname}.${condition.attribute}" has no column to filter on`);
211
+ }
212
+ if (!condition.values.length) {
213
+ linkConditions.push("1 = 0");
214
+ continue;
215
+ }
216
+ const escaped = condition.values.map((value) => queryGenerator.escape(value)).join(", ");
217
+ linkConditions.push(`${table}.${column} IN (${escaped})`);
218
+ }
219
+ if (!linkConditions.length) {
220
+ return no(`"${level.model.modelname}" has nothing to narrow it — the level would select every row`);
221
+ }
222
+ const conditions = [...linkConditions, ...guardConditions];
223
+ sql = `(SELECT ${table}.${selectColumn} FROM ${table} WHERE ${conditions.join(" AND ")})`;
224
+ }
225
+ return sql ? _Sequelize.literal(sql) : undefined;
226
+ }
119
227
  _buildJsonContainsCondition(targetKey, item) {
120
228
  const pattern = `%${JSON.stringify(item).replace(/[%_]/g, "\\$&")}%`;
121
229
  const attribute = this.model.rawAttributes?.[targetKey];
@@ -220,8 +328,15 @@ export class SequelizeModel extends AbstractModel {
220
328
  }
221
329
  // Handle operator objects like {contains: 'val'} from Adminizer's criteria format
222
330
  for (const [op, val] of Object.entries(value)) {
223
- if (val === undefined || val === null)
331
+ if (val === undefined || val === null) {
332
+ // Access scoping emits `in` lists, so an absent operand must fail
333
+ // closed (match nothing) rather than silently widen the query.
334
+ // Other operators keep the historical skip: filter builders rely on it.
335
+ if (op === "in") {
336
+ result[targetKey] = { [_Op.in]: [] };
337
+ }
224
338
  continue;
339
+ }
225
340
  switch (op) {
226
341
  case "eq":
227
342
  result[targetKey] = { [_Op.eq]: val };
@@ -303,6 +418,11 @@ export class SequelizeModel extends AbstractModel {
303
418
  case "between":
304
419
  result[targetKey] = { [_Op.between]: val };
305
420
  break;
421
+ case "intersects":
422
+ // Emitted only by the (unsupported) collection form of
423
+ // `userAccessRelation` — fail loudly instead of the default
424
+ // Op.eq fallback silently producing a wrong query.
425
+ throw new Error(`Sequelize adapter does not support the "intersects" operator (field "${String(targetKey)}")`);
306
426
  case "isNull":
307
427
  if (val) {
308
428
  result[targetKey] = { [_Op.is]: null };
@@ -355,7 +475,7 @@ export class SequelizeModel extends AbstractModel {
355
475
  // 'sort' as boolean/object = model field — goes to WHERE
356
476
  const criteriaSortValue = 'sort' in criteria ? criteria.sort : undefined;
357
477
  const isOrderingSort = typeof criteriaSortValue === 'string';
358
- const { where: nestedWhere, skip, limit, sort: criteriaSort, select, populate, ...rest } = criteria;
478
+ const { where: nestedWhere, skip, limit, sort: criteriaSort, select, populate, populateOn: _populateOn, ...rest } = criteria;
359
479
  // If 'sort' is a model field (not an ordering string), put it back
360
480
  if (!isOrderingSort && criteriaSortValue !== undefined) {
361
481
  rest.sort = criteriaSortValue;
@@ -499,11 +619,13 @@ export class SequelizeModel extends AbstractModel {
499
619
  const includes = criteria.populate
500
620
  ? this._buildCriteriaIncludes(criteria.populate)
501
621
  : this._buildIncludes();
622
+ this._applyPopulateOn(includes, criteria.populateOn);
623
+ const selectedAttributes = this._ensureForeignKeysSelected(attributes, criteria.populateOn);
502
624
  // console.debug(">> _findOne: converted where:", where);
503
625
  // console.debug(">> _findOne: includes:", includes);
504
626
  let instance = null;
505
627
  try {
506
- instance = await this.model.findOne({ where, attributes, include: includes });
628
+ instance = await this.model.findOne({ where, attributes: selectedAttributes, include: includes });
507
629
  // console.debug(">> _findOne: raw instance:", instance ? instance.toJSON() : null);
508
630
  }
509
631
  catch (err) {
@@ -515,6 +637,7 @@ export class SequelizeModel extends AbstractModel {
515
637
  return null;
516
638
  }
517
639
  const plain = instance.get({ plain: true });
640
+ this._collapsePopulateOn([plain], criteria.populateOn);
518
641
  // console.debug(">> _findOne: plain result:", plain);
519
642
  return plain;
520
643
  }
@@ -531,6 +654,8 @@ export class SequelizeModel extends AbstractModel {
531
654
  const includes = criteria.populate
532
655
  ? this._buildCriteriaIncludes(criteria.populate)
533
656
  : assocNames.map((alias) => this._buildListInclude(alias, usedRelationAliases, hasRelationPathCondition));
657
+ this._applyPopulateOn(includes, criteria.populateOn);
658
+ const selectedAttributes = this._ensureForeignKeysSelected(attributes, criteria.populateOn);
534
659
  // console.debug(">> _find: where, limit, offset, order, includes:", {
535
660
  // where,
536
661
  // limit,
@@ -541,33 +666,18 @@ export class SequelizeModel extends AbstractModel {
541
666
  let instances;
542
667
  instances = await this.model.findAll({
543
668
  where,
544
- attributes,
669
+ attributes: selectedAttributes,
545
670
  limit,
546
671
  offset,
547
672
  order,
548
673
  include: includes,
549
674
  subQuery: hasRelationPathCondition ? false : undefined
550
675
  });
551
- for (const inst of instances) {
552
- //For each association, we call getxxx () once again
553
- for (const alias of assocNames) {
554
- // @ts-ignore accessors is present
555
- const getAccessor = this.model.associations[alias].accessors.get;
556
- if (typeof inst[getAccessor] === "function") {
557
- try {
558
- const related = await inst[getAccessor]();
559
- const mapped = Array.isArray(related)
560
- ? related.map((r) => r.toJSON())
561
- : related?.toJSON();
562
- // console.debug(`---- get${alias}():`, mapped);
563
- }
564
- catch (e) {
565
- // console.error(`!! error when calling ${getAccessor}():`, e);
566
- }
567
- }
568
- }
569
- }
676
+ // Associations come from the `include` above: sequelize getters never write into
677
+ // dataValues, so calling them here would only cost one query per association per
678
+ // row and change nothing in the result.
570
679
  const plain = instances.map(i => i.get({ plain: true }));
680
+ this._collapsePopulateOn(plain, criteria.populateOn);
571
681
  // console.debug(">> _find: plain results:", plain);
572
682
  return plain;
573
683
  }
@@ -671,6 +781,50 @@ export class SequelizeModel extends AbstractModel {
671
781
  });
672
782
  return reloaded.map((r) => r.get({ plain: true }));
673
783
  }
784
+ /**
785
+ * Application-level cascade for the records `ids` identifies, one statement per
786
+ * association instead of a getter plus a delete per related row:
787
+ *
788
+ * - `belongsTo` — the parent is never touched; deleting a task must not delete its project;
789
+ * - `belongsToMany` — only the link rows go, never the far side: those rows are shared
790
+ * (deleting one post must not delete a tag another post still uses);
791
+ * - `hasOne` / `hasMany` — the children are deleted, which is the actual cascade.
792
+ *
793
+ * Row-level destroy hooks are preserved for targets that declare them.
794
+ */
795
+ async _cascadeDelete(ids) {
796
+ if (!ids.length) {
797
+ return;
798
+ }
799
+ for (const alias of Object.keys(this.model.associations)) {
800
+ const assoc = this.model.associations[alias];
801
+ if (assoc.associationType === "BelongsTo") {
802
+ continue;
803
+ }
804
+ const foreignKey = typeof assoc.foreignKey === "string" ? assoc.foreignKey : undefined;
805
+ if (!foreignKey) {
806
+ continue;
807
+ }
808
+ // Many-to-many: the link rows live in the through model, the targets are shared
809
+ const target = assoc.associationType === "BelongsToMany"
810
+ ? assoc.through?.model
811
+ : assoc.target;
812
+ if (!target?.destroy) {
813
+ continue;
814
+ }
815
+ try {
816
+ const hasRowHooks = typeof target.hasHook === "function"
817
+ && (target.hasHook("beforeDestroy") || target.hasHook("afterDestroy"));
818
+ await target.destroy({
819
+ where: { [foreignKey]: { [_Op.in]: ids } },
820
+ individualHooks: hasRowHooks,
821
+ });
822
+ }
823
+ catch (e) {
824
+ Adminizer.log.error(`Failed to cascade the deletion of "${this.modelname}.${alias}"`, e);
825
+ }
826
+ }
827
+ }
674
828
  // --- DESTROY ONE ---
675
829
  async _destroyOne(criteria) {
676
830
  await this._ensure();
@@ -678,31 +832,7 @@ export class SequelizeModel extends AbstractModel {
678
832
  const record = await this.model.findOne({ where });
679
833
  if (!record)
680
834
  return null;
681
- const assocNames = Object.keys(this.model.associations);
682
- for (const alias of assocNames) {
683
- const assoc = this.model.associations[alias];
684
- // @ts-ignore: accessors should exist
685
- const getAccessor = assoc.accessors?.get;
686
- if (typeof record[getAccessor] === "function") {
687
- try {
688
- const related = await record[getAccessor]();
689
- // 🧹Deleting related posts
690
- if (Array.isArray(related)) {
691
- for (const r of related) {
692
- if (typeof r.destroy === "function") {
693
- await r.destroy();
694
- }
695
- }
696
- }
697
- else if (related && typeof related.destroy === "function") {
698
- await related.destroy();
699
- }
700
- }
701
- catch (e) {
702
- // console.warn(`Failed to fetch/delete relation "${alias}":`, e);
703
- }
704
- }
705
- }
835
+ await this._cascadeDelete([record.get(this.model.primaryKeyAttribute)]);
706
836
  const raw = record.get({ plain: true });
707
837
  await record.destroy();
708
838
  return raw;
@@ -712,35 +842,7 @@ export class SequelizeModel extends AbstractModel {
712
842
  await this._ensure();
713
843
  const { where } = this._convertAdminizerCriteriaToSequelizeOptions(criteria);
714
844
  const records = await this.model.findAll({ where });
715
- const assocNames = Object.keys(this.model.associations);
716
- for (const record of records) {
717
- for (const alias of assocNames) {
718
- const assoc = this.model.associations[alias];
719
- // 🛑 We don’t touch parental ties
720
- if (assoc.associationType === "BelongsTo")
721
- continue;
722
- // @ts-ignore accessor exists
723
- const getAccessor = assoc.accessors?.get;
724
- if (typeof record[getAccessor] === "function") {
725
- try {
726
- const related = await record[getAccessor]();
727
- if (Array.isArray(related)) {
728
- for (const rel of related) {
729
- if (typeof rel.destroy === "function") {
730
- await rel.destroy();
731
- }
732
- }
733
- }
734
- else if (related && typeof related.destroy === "function") {
735
- await related.destroy();
736
- }
737
- }
738
- catch (e) {
739
- // console.warn(`Failed to delete relation ${alias}:`, e);
740
- }
741
- }
742
- }
743
- }
845
+ await this._cascadeDelete(records.map((r) => r.get(this.model.primaryKeyAttribute)));
744
846
  const raw = records.map((r) => r.get({ plain: true }));
745
847
  await this.model.destroy({ where });
746
848
  return raw;
@@ -825,6 +927,70 @@ export class SequelizeModel extends AbstractModel {
825
927
  _buildIncludes() {
826
928
  return Object.keys(this.model.associations).map(key => ({ association: key }));
827
929
  }
930
+ /**
931
+ * `criteria.populateOn` support: BelongsTo only — the owning row keeps its FK
932
+ * column, so a record filtered out of the JOIN can be handed back as that bare value.
933
+ */
934
+ canPushdownPopulateAccess(association) {
935
+ const assoc = this.model.associations[association];
936
+ return Boolean(assoc
937
+ && assoc.associationType === "BelongsTo"
938
+ && typeof assoc.foreignKey === "string"
939
+ && this.model.rawAttributes[assoc.foreignKey]);
940
+ }
941
+ /** ANDs `criteria.populateOn` into the matching includes' ON clause (LEFT JOIN — owning rows stay). */
942
+ _applyPopulateOn(includes, populateOn) {
943
+ if (!populateOn) {
944
+ return;
945
+ }
946
+ for (const [association, on] of Object.entries(populateOn)) {
947
+ const include = includes.find((entry) => entry.association === association);
948
+ if (!include) {
949
+ continue;
950
+ }
951
+ const condition = this._convertCriteriaToSequelize(on);
952
+ if (include.where) {
953
+ // An explicit populate-where keeps filtering the owning rows (INNER JOIN),
954
+ // now matching only records inside the access confinement.
955
+ include.where = { [_Op.and]: [include.where, condition] };
956
+ }
957
+ else {
958
+ include.where = condition;
959
+ include.required = false;
960
+ }
961
+ }
962
+ }
963
+ /** A record filtered out of a `populateOn` JOIN comes back as its bare foreign key. */
964
+ _collapsePopulateOn(records, populateOn) {
965
+ if (!populateOn) {
966
+ return;
967
+ }
968
+ for (const association of Object.keys(populateOn)) {
969
+ const foreignKey = this.model.associations[association]?.foreignKey;
970
+ if (typeof foreignKey !== "string") {
971
+ continue;
972
+ }
973
+ for (const record of records) {
974
+ if (record[association] == null && record[foreignKey] != null) {
975
+ record[association] = record[foreignKey];
976
+ }
977
+ }
978
+ }
979
+ }
980
+ /** The collapse above reads the FK columns — keep them selected under an explicit `select`. */
981
+ _ensureForeignKeysSelected(attributes, populateOn) {
982
+ if (!attributes || !populateOn) {
983
+ return attributes;
984
+ }
985
+ const result = [...attributes];
986
+ for (const association of Object.keys(populateOn)) {
987
+ const foreignKey = this.model.associations[association]?.foreignKey;
988
+ if (typeof foreignKey === "string" && !result.includes(foreignKey)) {
989
+ result.push(foreignKey);
990
+ }
991
+ }
992
+ return result;
993
+ }
828
994
  _buildAttributes(select, rawAttributes = this.model.rawAttributes) {
829
995
  if (!select) {
830
996
  return undefined;
@@ -28,6 +28,22 @@ export declare class TypeOrmModel<T extends ObjectLiteral> extends AbstractModel
28
28
  private normalizeCriteria;
29
29
  private addRequestedRelations;
30
30
  private addAllRelationJoins;
31
+ /**
32
+ * `criteria.populateOn` support: many-to-one with its FK declared as a real column
33
+ * property — the hydrated entity then carries the value the filtered association
34
+ * collapses to. (An undeclared FK leaves `joinColumns[0].propertyName` equal to the
35
+ * relation's own property name.)
36
+ */
37
+ canPushdownPopulateAccess(association: string): boolean;
38
+ /**
39
+ * Renders a `populateOn` entry into the JOIN's ON clause. Only the shapes the
40
+ * record-access layer marks pushdown-safe (primitive equality, `{in: [...]}`) are
41
+ * supported; anything else fails closed to a never-matching condition — the
42
+ * association then collapses to its foreign key rather than leaking.
43
+ */
44
+ private buildJoinCondition;
45
+ /** A record filtered out of a `populateOn` JOIN comes back as its bare foreign key. */
46
+ private collapsePopulateOn;
31
47
  private isNativeWhere;
32
48
  private applyFindOptions;
33
49
  private applyWhere;
@@ -1,7 +1,11 @@
1
1
  import { Brackets, EntitySchema, NotBrackets, } from "typeorm";
2
2
  import { v4 as uuid } from "uuid";
3
3
  import { AbstractAdapter, AbstractModel } from "../AbstractModel.js";
4
+ import { Adminizer } from "../../Adminizer.js";
4
5
  const ROOT_ALIAS = "record";
6
+ /** Always-false / always-true predicates: an empty IN-list must not depend on driver quirks. */
7
+ const MATCH_NOTHING = "1 = 0";
8
+ const MATCH_EVERYTHING = "1 = 1";
5
9
  function normalizeModelName(modelName) {
6
10
  return modelName.toLowerCase();
7
11
  }
@@ -295,7 +299,7 @@ export class TypeOrmModel extends AbstractModel {
295
299
  normalizeCriteria(criteria = {}) {
296
300
  const criteriaSortValue = "sort" in criteria ? criteria.sort : undefined;
297
301
  const isOrderingSort = typeof criteriaSortValue === "string";
298
- const { where: nestedWhere, skip, limit, sort, select, populate, ...rest } = criteria;
302
+ const { where: nestedWhere, skip, limit, sort, select, populate, populateOn: _populateOn, ...rest } = criteria;
299
303
  if (!isOrderingSort && criteriaSortValue !== undefined) {
300
304
  rest.sort = criteriaSortValue;
301
305
  }
@@ -308,7 +312,7 @@ export class TypeOrmModel extends AbstractModel {
308
312
  select: this.buildAttributes(select),
309
313
  };
310
314
  }
311
- addRequestedRelations(qb, populate) {
315
+ addRequestedRelations(qb, populate, populateOn) {
312
316
  const aliases = new Map();
313
317
  const relations = populate ? Object.keys(populate) : this.relationNames;
314
318
  for (const relationName of relations) {
@@ -316,7 +320,15 @@ export class TypeOrmModel extends AbstractModel {
316
320
  continue;
317
321
  }
318
322
  const alias = `${ROOT_ALIAS}_${relationName}`;
319
- qb.leftJoinAndSelect(`${ROOT_ALIAS}.${relationName}`, alias);
323
+ const joinCondition = populateOn?.[relationName]
324
+ ? this.buildJoinCondition(populateOn[relationName], alias)
325
+ : undefined;
326
+ if (joinCondition) {
327
+ qb.leftJoinAndSelect(`${ROOT_ALIAS}.${relationName}`, alias, joinCondition.sql, joinCondition.params);
328
+ }
329
+ else {
330
+ qb.leftJoinAndSelect(`${ROOT_ALIAS}.${relationName}`, alias);
331
+ }
320
332
  aliases.set(relationName, alias);
321
333
  }
322
334
  if (populate) {
@@ -353,17 +365,93 @@ export class TypeOrmModel extends AbstractModel {
353
365
  }
354
366
  return aliases;
355
367
  }
368
+ /**
369
+ * `criteria.populateOn` support: many-to-one with its FK declared as a real column
370
+ * property — the hydrated entity then carries the value the filtered association
371
+ * collapses to. (An undeclared FK leaves `joinColumns[0].propertyName` equal to the
372
+ * relation's own property name.)
373
+ */
374
+ canPushdownPopulateAccess(association) {
375
+ const relation = this.getRelation(association);
376
+ const joinColumn = relation?.joinColumns[0];
377
+ return Boolean(relation?.isManyToOne
378
+ && joinColumn?.propertyName
379
+ && joinColumn.propertyName !== relation.propertyName
380
+ && this.metadata.columns.some((column) => column.propertyName === joinColumn.propertyName));
381
+ }
382
+ /**
383
+ * Renders a `populateOn` entry into the JOIN's ON clause. Only the shapes the
384
+ * record-access layer marks pushdown-safe (primitive equality, `{in: [...]}`) are
385
+ * supported; anything else fails closed to a never-matching condition — the
386
+ * association then collapses to its foreign key rather than leaking.
387
+ */
388
+ buildJoinCondition(on, alias) {
389
+ const expressions = [];
390
+ const params = {};
391
+ for (const [field, value] of Object.entries(on)) {
392
+ if (value === null) {
393
+ expressions.push(`${alias}.${field} IS NULL`);
394
+ continue;
395
+ }
396
+ if (["string", "number", "boolean"].includes(typeof value)) {
397
+ const param = this.nextParamName();
398
+ expressions.push(`${alias}.${field} = :${param}`);
399
+ params[param] = value;
400
+ continue;
401
+ }
402
+ const inList = value && typeof value === "object" && Object.keys(value).length === 1
403
+ ? value.in
404
+ : undefined;
405
+ if (Array.isArray(inList)) {
406
+ if (!inList.length) {
407
+ expressions.push(MATCH_NOTHING);
408
+ continue;
409
+ }
410
+ const param = this.nextParamName();
411
+ expressions.push(`${alias}.${field} IN (:...${param})`);
412
+ params[param] = inList;
413
+ continue;
414
+ }
415
+ expressions.push(MATCH_NOTHING);
416
+ }
417
+ return { sql: expressions.join(" AND ") || MATCH_NOTHING, params };
418
+ }
419
+ /** A record filtered out of a `populateOn` JOIN comes back as its bare foreign key. */
420
+ collapsePopulateOn(entities, populateOn) {
421
+ if (!populateOn) {
422
+ return;
423
+ }
424
+ for (const association of Object.keys(populateOn)) {
425
+ const fkProperty = this.getRelation(association)?.joinColumns[0]?.propertyName;
426
+ if (!fkProperty || fkProperty === association) {
427
+ continue;
428
+ }
429
+ for (const entity of entities) {
430
+ if (entity[association] == null && entity[fkProperty] != null) {
431
+ entity[association] = entity[fkProperty];
432
+ }
433
+ }
434
+ }
435
+ }
356
436
  isNativeWhere(where) {
357
437
  return where instanceof Brackets || where instanceof NotBrackets;
358
438
  }
359
439
  applyFindOptions(qb, criteria) {
360
440
  const options = this.normalizeCriteria(criteria);
361
- const aliases = this.addRequestedRelations(qb, criteria.populate);
441
+ const aliases = this.addRequestedRelations(qb, criteria.populate, criteria.populateOn);
362
442
  if (this.isNativeWhere(options.where) || Object.keys(options.where).length > 0) {
363
443
  this.applyWhere(qb, options.where, ROOT_ALIAS, aliases);
364
444
  }
365
445
  if (options.select) {
366
- qb.select(options.select.map((field) => `${ROOT_ALIAS}.${field}`));
446
+ // The populateOn collapse reads the FK properties — keep them selected.
447
+ const selected = [...options.select];
448
+ for (const association of Object.keys(criteria.populateOn ?? {})) {
449
+ const fkProperty = this.getRelation(association)?.joinColumns[0]?.propertyName;
450
+ if (fkProperty && fkProperty !== association && !selected.includes(fkProperty)) {
451
+ selected.push(fkProperty);
452
+ }
453
+ }
454
+ qb.select(selected.map((field) => `${ROOT_ALIAS}.${field}`));
367
455
  }
368
456
  for (const [field, direction] of options.order) {
369
457
  const orderColumn = this.getOrderColumn(ROOT_ALIAS, field);
@@ -389,7 +477,16 @@ export class TypeOrmModel extends AbstractModel {
389
477
  }
390
478
  }
391
479
  buildWhereBrackets(where, alias, relationAliases) {
392
- const entries = Object.entries(where).filter(([, value]) => value !== undefined);
480
+ const entries = Object.entries(where).filter(([key, value]) => {
481
+ if (value === undefined) {
482
+ // Historical behaviour: an undefined value drops the condition. Record-access
483
+ // filters never emit one, so this only fires for hand-built criteria — but a
484
+ // dropped condition widens the query, so it must not stay silent.
485
+ Adminizer.log.warn(`TypeORM adapter: criteria field "${key}" has an undefined value and was dropped`);
486
+ return false;
487
+ }
488
+ return true;
489
+ });
393
490
  if (!entries.length) {
394
491
  return undefined;
395
492
  }
@@ -467,6 +564,9 @@ export class TypeOrmModel extends AbstractModel {
467
564
  return { sql: `${column} IS NULL` };
468
565
  }
469
566
  if (Array.isArray(value)) {
567
+ if (!value.length) {
568
+ return { sql: MATCH_NOTHING };
569
+ }
470
570
  const param = this.nextParamName();
471
571
  return { sql: `${column} IN (:...${param})`, params: { [param]: value } };
472
572
  }
@@ -474,8 +574,15 @@ export class TypeOrmModel extends AbstractModel {
474
574
  const expressions = [];
475
575
  const params = {};
476
576
  for (const [operator, raw] of Object.entries(value)) {
477
- if (raw === undefined)
577
+ if (raw === undefined || raw === null) {
578
+ // Access scoping emits `in` lists, so an absent operand must fail
579
+ // closed (match nothing) rather than silently widen the query.
580
+ // Other operators keep the historical skip: filter builders rely on it.
581
+ if (operator === "in" || operator === "$in") {
582
+ expressions.push(MATCH_NOTHING);
583
+ }
478
584
  continue;
585
+ }
479
586
  const built = this.buildOperatorCondition(column, operator, raw);
480
587
  if (!built)
481
588
  continue;
@@ -528,10 +635,18 @@ export class TypeOrmModel extends AbstractModel {
528
635
  return { sql: `CAST(${column} AS TEXT) LIKE :${param}`, params: { [param]: `%${value}` } };
529
636
  case "in":
530
637
  case "$in":
638
+ // An empty list matches nothing on every driver; the spread placeholder
639
+ // would depend on TypeORM's empty-array handling.
640
+ if (Array.isArray(value) && !value.length) {
641
+ return { sql: MATCH_NOTHING };
642
+ }
531
643
  return { sql: `${column} IN (:...${param})`, params };
532
644
  case "notIn":
533
645
  case "nin":
534
646
  case "$notIn":
647
+ if (Array.isArray(value) && !value.length) {
648
+ return { sql: MATCH_EVERYTHING };
649
+ }
535
650
  return { sql: `${column} NOT IN (:...${param})`, params };
536
651
  case "between":
537
652
  case "$between": {
@@ -551,7 +666,13 @@ export class TypeOrmModel extends AbstractModel {
551
666
  const serialized = JSON.stringify(value).replace(/[%_]/g, "\\$&");
552
667
  return { sql: `CAST(${column} AS TEXT) LIKE :${param}`, params: { [param]: `%${serialized}%` } };
553
668
  }
669
+ case "intersects":
670
+ // Emitted only by the (unsupported) collection form of
671
+ // `userAccessRelation` — fail loudly instead of silently dropping the
672
+ // condition, which would widen the query (sequelize adapter parity).
673
+ throw new Error(`TypeORM adapter does not support the "intersects" operator (column "${column}")`);
554
674
  default:
675
+ Adminizer.log.warn(`TypeORM adapter: unsupported criteria operator "${operator}" was dropped for column "${column}"`);
555
676
  return undefined;
556
677
  }
557
678
  }
@@ -660,12 +781,18 @@ export class TypeOrmModel extends AbstractModel {
660
781
  async _findOne(criteria = {}) {
661
782
  const qb = this.createQueryBuilder();
662
783
  this.applyFindOptions(qb, criteria);
663
- return await qb.getOne();
784
+ const record = await qb.getOne();
785
+ if (record) {
786
+ this.collapsePopulateOn([record], criteria.populateOn);
787
+ }
788
+ return record;
664
789
  }
665
790
  async _find(criteria = {}) {
666
791
  const qb = this.createQueryBuilder();
667
792
  this.applyFindOptions(qb, criteria);
668
- return await qb.getMany();
793
+ const records = await qb.getMany();
794
+ this.collapsePopulateOn(records, criteria.populateOn);
795
+ return records;
669
796
  }
670
797
  async _updateOne(criteria, data) {
671
798
  const record = await this._findOne(criteria);