adminizer 5.0.0-build.8 → 5.1.0-build.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/abnfDiagram-VCTEODGH-DFY3Pztx.js +1 -0
- package/assets/add-31tS-g4V.js +1 -0
- package/assets/add-group-Cf3zT7WE.js +1 -0
- package/assets/add-user-C62wOna-.js +1 -0
- package/assets/ai-assistant/agent.es.js +28464 -0
- package/assets/app-DPmI-MvN.css +1 -0
- package/assets/app-vdHbjOQX.js +138 -0
- package/assets/arc-DP4o1zz3.js +1 -0
- package/assets/architectureDiagram-5GKGNRK7-BjXW5Zu0.js +36 -0
- package/assets/asmcrypto-DZeQH0LC.js +1 -0
- package/assets/blockDiagram-I7D4REHJ-BUPnDwN0.js +129 -0
- package/assets/c4Diagram-7LVT6UL2-Cmb_dAhd.js +38 -0
- package/assets/{catalog-BEvJZYJP.js → catalog-C-y5rMaG.js} +3 -3
- package/assets/channel-CkJRUsq_.js +1 -0
- package/assets/chunk-2Q5K7J3B-CmS-dWwQ.js +1 -0
- package/assets/chunk-5VM5RSS4-Cqs2Ow64.js +15 -0
- package/assets/chunk-F27PBJKO-BoMCpJA1.js +1 -0
- package/assets/chunk-IMKFNOWR-DlDcPk1x.js +231 -0
- package/assets/chunk-JWPE2WC7-DzL_SU8s.js +1 -0
- package/assets/chunk-POPQ4Y6H-Cwq2-Vb8.js +1 -0
- package/assets/chunk-SVP7TREG-BfRHY5FU.js +88 -0
- package/assets/chunk-TICWLB2K-CRP7rHkW.js +206 -0
- package/assets/chunk-XXDRQBXY-Bg2jg-98.js +1 -0
- package/assets/classDiagram-ZZMXUADV-D4EF3HGj.js +1 -0
- package/assets/classDiagram-v2-VYDZK3BY-D4EF3HGj.js +1 -0
- package/assets/controls/ckeditor.es.js +9382 -9320
- package/assets/controls/handsontable.es.js +11971 -11938
- package/assets/controls/jsoneditor.es.js +10442 -10233
- package/assets/controls/toast-ui.es.js +1308 -1279
- package/assets/cose-bilkent-JH36ORCC-CCJ0SGyO.js +1 -0
- package/assets/cynefin-OW5HDTMX-B2Jx5fw3.js +166 -0
- package/assets/cynefinDiagram-5FMLGOSQ-DuK0Lb62.js +62 -0
- package/assets/cytoscape.esm-DzSz-X2X.js +321 -0
- package/assets/dagre-GXQ25YYZ-DnOYexET.js +4 -0
- package/assets/dashboard-X8k4X6Sw.js +3 -0
- package/assets/debounce-B3mXI_sx.js +1 -0
- package/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/assets/diagram-S7CK7UJ4-DDgPBHsK.js +30 -0
- package/assets/diagram-UQ7AKVKN-qlL0GZs8.js +41 -0
- package/assets/diagram-VSXAHHWV-CQbiU1_c.js +3 -0
- package/assets/diagram-VX7I27RA-DFDI9_au.js +24 -0
- package/assets/diagram-Z3DM3KII-B5C9dtmJ.js +24 -0
- package/assets/docs-BiZOD7PP.js +1 -0
- package/assets/ebnfDiagram-PWID7BFC-CLMUsXEI.js +1 -0
- package/assets/erDiagram-RLTQ6QDP-ax9Xol5-.js +99 -0
- package/assets/flowDiagram-HODETNUW-CvaH7l3S.js +168 -0
- package/assets/ganttDiagram-EL5Y4UJY-Cga5U19j.js +292 -0
- package/assets/gitGraphDiagram-WWUBYQGX-B6o1T3-d.js +106 -0
- package/assets/history-CVBJUNSQ.js +1 -0
- package/assets/infoDiagram-27XIBGKW-BpWxgBCE.js +2 -0
- package/assets/init-Gi6I4Gst.js +1 -0
- package/assets/init-user-xSvnFTF0.js +1 -0
- package/assets/isObject-DL-BuaBl.js +1 -0
- package/assets/ishikawaDiagram-5VMMS53U-zE83db4H.js +70 -0
- package/assets/journeyDiagram-3NMN7TZE-DZNVFqrX.js +139 -0
- package/assets/kanban-definition-UXKFOSKX-CqxNgk27.js +89 -0
- package/assets/katex-HP8lGamR.js +257 -0
- package/assets/layout-CN3kt_jt.js +1 -0
- package/assets/linear-nvkaQ6BA.js +1 -0
- package/assets/{list-DhK0McVn.js → list-CrIU2eZz.js} +2 -2
- package/assets/login-CX9xVjCn.js +1 -0
- package/assets/manifest.json +834 -58
- package/assets/media-manager-BfYftLIH.js +6 -0
- package/assets/mermaid.core-C0V1NaVJ.js +310 -0
- package/assets/mindmap-definition-YA3MSWOX-Bh7_d9f1.js +96 -0
- package/assets/module-DPEvnad3.js +1 -0
- package/assets/notification-Dqpt5AIZ.js +20 -0
- package/assets/ordinal-Cboi1Yqb.js +1 -0
- package/assets/pegDiagram-XKGWAZYB-DMnjm6Z1.js +1 -0
- package/assets/pieDiagram-E7YTZNPT-D-RWCgYb.js +39 -0
- package/assets/quadrantDiagram-AXDQQJYC-BNBGP7RM.js +7 -0
- package/assets/railroadDiagram-O6MQD6OU-BIjVPT01.js +1 -0
- package/assets/register-CF4KQ97e.js +1 -0
- package/assets/requirementDiagram-BXWQKSXE-VPrtGdjM.js +84 -0
- package/assets/ru-CFJ4fysR.js +1 -0
- package/assets/sankeyDiagram-P5KCCOFB-ct9mLF3U.js +40 -0
- package/assets/sequenceDiagram-WJ2MYXX4-TC06Mbmw.js +162 -0
- package/assets/sizeCapture-INFHLROL-BMMwriCf.js +1 -0
- package/assets/stateDiagram-D77RDMKH-DtwYRPCX.js +1 -0
- package/assets/stateDiagram-v2-MP3YSRHH-C1yB0O5U.js +1 -0
- package/assets/swimlanes-42K2YHIH-DE3hRbbr.js +1 -0
- package/assets/swimlanesDiagram-VR7AAH4N-Ce4l1sQD.js +8 -0
- package/assets/timeline-definition-24CTP7MA-N7s_hfjj.js +120 -0
- package/assets/toNumber-DZb-r9sy.js +1 -0
- package/assets/use-appearance-DYTIKpT2.js +1 -0
- package/assets/user-filters-list-fR0YNJ3l.js +1 -0
- package/assets/vennDiagram-4TSXK5OY-Dj5bFQkR.js +34 -0
- package/assets/wardleyDiagram-VM6X3IG4-83ax25Z6.js +78 -0
- package/assets/welcome-v11MTsRK.js +1 -0
- package/assets/with-app-layout-CH6RIib1.js +31 -0
- package/assets/xychartDiagram-S5SC5T6Z-CW9Br5Xp.js +7 -0
- package/controllers/access-rights/permissionOptions.d.ts +2 -0
- package/controllers/access-rights/permissionOptions.js +23 -0
- package/controllers/add.js +2 -2
- package/controllers/addGroup.js +15 -6
- package/controllers/ai/AiAgentController.d.ts +66 -0
- package/controllers/ai/AiAgentController.js +435 -0
- package/controllers/ai/AiAssistantController.d.ts +0 -4
- package/controllers/ai/AiAssistantController.js +4 -78
- package/controllers/catalog/FrontendCatalogAdapter.js +7 -8
- package/controllers/docs/index.d.ts +22 -0
- package/controllers/docs/index.js +126 -0
- package/controllers/edit.js +2 -2
- package/controllers/editGroup.js +15 -6
- package/controllers/filter-fields/savedFilters.js +1 -1
- package/controllers/history-actions/HistoryController.js +7 -7
- package/controllers/inlineUpdate.js +10 -3
- package/controllers/list.js +1 -1
- package/controllers/login.js +1 -1
- package/controllers/notifications/NotificationController.js +14 -8
- package/controllers/view.js +3 -3
- package/controllers/widgets/filterInfo.js +1 -1
- package/helpers/accessRightsHelper.d.ts +30 -3
- package/helpers/accessRightsHelper.js +140 -12
- package/helpers/assetVersionHelper.d.ts +14 -0
- package/helpers/assetVersionHelper.js +42 -0
- package/helpers/configHelper.js +19 -18
- package/helpers/controllerHelper.d.ts +1 -0
- package/helpers/controllerHelper.js +25 -15
- package/helpers/fieldsHelper.d.ts +2 -1
- package/helpers/fieldsHelper.js +11 -5
- package/helpers/inertiaActionsHelper.d.ts +1 -1
- package/helpers/inertiaActionsHelper.js +4 -5
- package/helpers/inertiaAddHelper.d.ts +1 -1
- package/helpers/inertiaAddHelper.js +7 -6
- package/helpers/inertiaGroupHelper.js +10 -2
- package/helpers/inertiaListHelper.d.ts +1 -1
- package/helpers/inertiaListHelper.js +7 -8
- package/helpers/inertiaMenuHelper.d.ts +7 -2
- package/helpers/inertiaMenuHelper.js +12 -27
- package/helpers/menuHelper.d.ts +11 -1
- package/helpers/menuHelper.js +12 -0
- package/helpers/modelResourceHelper.d.ts +9 -0
- package/helpers/modelResourceHelper.js +43 -0
- package/helpers/navigationAccessHelper.d.ts +13 -0
- package/helpers/navigationAccessHelper.js +45 -0
- package/index.d.ts +11 -0
- package/index.js +11 -0
- package/interfaces/adminpanelConfig.d.ts +160 -2
- package/interfaces/queryCriteria.d.ts +13 -0
- package/interfaces/types.d.ts +169 -0
- package/lib/Adminizer.d.ts +8 -0
- package/lib/Adminizer.js +29 -2
- package/lib/DataAccessor.d.ts +100 -3
- package/lib/DataAccessor.js +454 -245
- package/lib/access-graph/AccessGraphResolver.d.ts +136 -0
- package/lib/access-graph/AccessGraphResolver.js +605 -0
- package/lib/access-graph/RecordAccessCache.d.ts +23 -0
- package/lib/access-graph/RecordAccessCache.js +30 -0
- package/lib/access-graph/RecordAccessResolver.d.ts +83 -0
- package/lib/access-graph/RecordAccessResolver.js +295 -0
- package/lib/access-graph/shared.d.ts +60 -0
- package/lib/access-graph/shared.js +121 -0
- package/lib/admin-links/AdminLinkHandler.d.ts +72 -0
- package/lib/admin-links/AdminLinkHandler.js +183 -0
- package/lib/ai-assistant/AbstractAiConversationHistoryService.d.ts +39 -0
- package/lib/ai-assistant/AbstractAiConversationHistoryService.js +98 -0
- package/lib/ai-assistant/AbstractAiModelService.d.ts +139 -2
- package/lib/ai-assistant/AbstractAiModelService.js +154 -1
- package/lib/ai-assistant/AiAgentRunManager.d.ts +35 -0
- package/lib/ai-assistant/AiAgentRunManager.js +109 -0
- package/lib/ai-assistant/AiAssistantAgentSkillHandler.d.ts +61 -0
- package/lib/ai-assistant/AiAssistantAgentSkillHandler.js +90 -0
- package/lib/ai-assistant/AiAssistantHandler.d.ts +1 -0
- package/lib/ai-assistant/AiAssistantHandler.js +4 -0
- package/lib/ai-assistant/AiAssistantUiMethodHandler.d.ts +60 -0
- package/lib/ai-assistant/AiAssistantUiMethodHandler.js +144 -0
- package/lib/ai-assistant/builtinAgentSkills.d.ts +9 -0
- package/lib/ai-assistant/builtinAgentSkills.js +259 -0
- package/lib/ai-assistant/jsonSafe.d.ts +13 -0
- package/lib/ai-assistant/jsonSafe.js +81 -0
- package/lib/app-manager/AdminizerApp.d.ts +31 -2
- package/lib/app-manager/AppManager.js +43 -42
- package/lib/app-manager/ConfigLayerHandler.js +4 -0
- package/lib/docs/AbstractDocumentation.d.ts +100 -0
- package/lib/docs/AbstractDocumentation.js +108 -0
- package/lib/docs/DocumentationHandler.d.ts +100 -0
- package/lib/docs/DocumentationHandler.js +198 -0
- package/lib/docs/FileDocumentation.d.ts +47 -0
- package/lib/docs/FileDocumentation.js +189 -0
- package/lib/docs/docsAgentSkills.d.ts +10 -0
- package/lib/docs/docsAgentSkills.js +93 -0
- package/lib/filters/FilterService.js +4 -3
- package/lib/history-actions/AbstractHistoryAdapter.d.ts +26 -1
- package/lib/history-actions/AbstractHistoryAdapter.js +155 -25
- package/lib/media-manager/helpers/MediaManagerHelper.js +9 -7
- package/lib/model/AbstractModel.d.ts +48 -0
- package/lib/model/AbstractModel.js +14 -4
- package/lib/model/ModelHandler.d.ts +50 -2
- package/lib/model/ModelHandler.js +117 -8
- package/lib/model/adapter/sequelize.d.ts +43 -3
- package/lib/model/adapter/sequelize.js +317 -138
- package/lib/model/adapter/typeorm.d.ts +16 -0
- package/lib/model/adapter/typeorm.js +136 -9
- package/lib/notifications/GeneralNotificationService.js +6 -5
- package/lib/notifications/SystemNotificationService.js +1 -1
- package/lib/ui-i18n/uiTranslationKeys.d.ts +2 -1
- package/lib/ui-i18n/uiTranslationKeys.js +42 -1
- package/lib/widgets/widgetHandler.js +10 -6
- package/models/Group.d.ts +2 -1
- package/package.json +35 -23
- package/policies/authPolicies.js +6 -6
- package/policies/permissionResolvers.d.ts +1 -0
- package/policies/permissionResolvers.js +3 -0
- package/system/Router.js +21 -3
- package/system/bindAccessRights.js +13 -3
- package/system/bindDocs.d.ts +13 -0
- package/system/bindDocs.js +77 -0
- package/system/bindInertia.js +30 -7
- package/system/buildInternalModelAccess.d.ts +8 -0
- package/system/buildInternalModelAccess.js +36 -4
- package/system/defaults.js +7 -1
- package/system/validateSystemModels.js +29 -9
- package/assets/add-DED_wpga.js +0 -1
- package/assets/add-group-CL80RdYb.js +0 -1
- package/assets/add-user-BRmwvQB8.js +0 -1
- package/assets/app-DNXiApCk.css +0 -1
- package/assets/app-layout-86yxiZOy.js +0 -1
- package/assets/app.js +0 -138
- package/assets/dashboard-ZP6K2s_8.js +0 -3
- package/assets/debounce-BMmXVQ06.js +0 -1
- package/assets/history-M61Bcv02.js +0 -1
- package/assets/init-user-8aUohVDq.js +0 -1
- package/assets/login-CO4laf6m.js +0 -1
- package/assets/media-manager-BDbpSSjH.js +0 -6
- package/assets/module-DfBneaUl.js +0 -1
- package/assets/notification-CVnQf3ef.js +0 -20
- package/assets/register-DBYIMXGQ.js +0 -1
- package/assets/ru-rYKdQbWF.js +0 -1
- package/assets/use-appearance-_EbbjHTh.js +0 -1
- package/assets/user-filters-list-DXhGLFTn.js +0 -1
- package/assets/welcome-CLxvXkcg.js +0 -1
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
let Sequelize;
|
|
2
|
-
let Op;
|
|
3
|
-
let HasMany;
|
|
4
1
|
import { AbstractAdapter, AbstractModel } from "../AbstractModel.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Sequelize = stub;
|
|
18
|
-
Op = stub;
|
|
19
|
-
HasMany = stub;
|
|
2
|
+
import { Adminizer } from "../../Adminizer.js";
|
|
3
|
+
let _Sequelize = null;
|
|
4
|
+
let _Op = null;
|
|
5
|
+
let _HasMany = null;
|
|
6
|
+
async function ensureSequelize() {
|
|
7
|
+
if (_Sequelize)
|
|
8
|
+
return _Sequelize;
|
|
9
|
+
const sequelize = await import('sequelize');
|
|
10
|
+
_Sequelize = sequelize.Sequelize;
|
|
11
|
+
_Op = sequelize.Op;
|
|
12
|
+
_HasMany = sequelize.HasMany;
|
|
13
|
+
return _Sequelize;
|
|
20
14
|
}
|
|
21
15
|
function resolveType(type) {
|
|
22
16
|
const sqlType = typeof type.toString === "function"
|
|
@@ -112,15 +106,129 @@ function capitalize(str) {
|
|
|
112
106
|
}
|
|
113
107
|
export class SequelizeModel extends AbstractModel {
|
|
114
108
|
model;
|
|
109
|
+
_init = false;
|
|
110
|
+
async _ensure() {
|
|
111
|
+
if (!this._init) {
|
|
112
|
+
await ensureSequelize();
|
|
113
|
+
this._init = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
115
116
|
constructor(modelName, model) {
|
|
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];
|
|
122
230
|
const columnName = typeof attribute?.field === "string" ? attribute.field : targetKey;
|
|
123
|
-
return
|
|
231
|
+
return _Sequelize.where(_Sequelize.cast(_Sequelize.col(`${this.model.name}.${columnName}`), "TEXT"), { [_Op.like]: pattern });
|
|
124
232
|
}
|
|
125
233
|
_convertCriteriaToSequelize(criteria) {
|
|
126
234
|
// Special handling for Sequelize.literal() at the top level
|
|
@@ -155,24 +263,24 @@ export class SequelizeModel extends AbstractModel {
|
|
|
155
263
|
}
|
|
156
264
|
// Special handling for null values (IS NULL condition)
|
|
157
265
|
if (value === null) {
|
|
158
|
-
result[targetKey] = { [
|
|
266
|
+
result[targetKey] = { [_Op.is]: null };
|
|
159
267
|
continue;
|
|
160
268
|
}
|
|
161
269
|
// 🔹 Handle Sequelize Op operators (already in Sequelize format)
|
|
162
|
-
if (key ===
|
|
163
|
-
result[
|
|
270
|
+
if (key === _Op.and || key === 'and') {
|
|
271
|
+
result[_Op.and] = Array.isArray(value)
|
|
164
272
|
? value.map((item) => this._convertCriteriaToSequelize(item))
|
|
165
273
|
: [];
|
|
166
274
|
continue;
|
|
167
275
|
}
|
|
168
|
-
if (key ===
|
|
169
|
-
result[
|
|
276
|
+
if (key === _Op.or || key === 'or') {
|
|
277
|
+
result[_Op.or] = Array.isArray(value)
|
|
170
278
|
? value.map((item) => this._convertCriteriaToSequelize(item))
|
|
171
279
|
: [];
|
|
172
280
|
continue;
|
|
173
281
|
}
|
|
174
|
-
if (key ===
|
|
175
|
-
result[
|
|
282
|
+
if (key === _Op.not || key === 'not' || key === '$not') {
|
|
283
|
+
result[_Op.not] = this._convertCriteriaToSequelize(value);
|
|
176
284
|
continue;
|
|
177
285
|
}
|
|
178
286
|
// Skip Symbol keys that are not handled above (should not happen, but safety check)
|
|
@@ -181,7 +289,7 @@ export class SequelizeModel extends AbstractModel {
|
|
|
181
289
|
}
|
|
182
290
|
if (Array.isArray(value)) {
|
|
183
291
|
// ✅ Array processing - use the IN operator
|
|
184
|
-
result[targetKey] = { [
|
|
292
|
+
result[targetKey] = { [_Op.in]: value };
|
|
185
293
|
}
|
|
186
294
|
else if (typeof value === "object" && !Array.isArray(value)) {
|
|
187
295
|
// Check if value already contains Sequelize Op operators
|
|
@@ -191,10 +299,10 @@ export class SequelizeModel extends AbstractModel {
|
|
|
191
299
|
// Helper function to check if a symbol/op is a Sequelize operator
|
|
192
300
|
const isSequelizeOp = (op) => {
|
|
193
301
|
// Check direct Symbol equality first
|
|
194
|
-
if (op ===
|
|
195
|
-
op ===
|
|
196
|
-
op ===
|
|
197
|
-
op ===
|
|
302
|
+
if (op === _Op.gt || op === _Op.gte || op === _Op.lt || op === _Op.lte ||
|
|
303
|
+
op === _Op.eq || op === _Op.ne || op === _Op.in || op === _Op.notIn ||
|
|
304
|
+
op === _Op.like || op === _Op.iLike || op === _Op.between ||
|
|
305
|
+
op === _Op.startsWith || op === _Op.endsWith || op === _Op.is || op === _Op.not) {
|
|
198
306
|
return true;
|
|
199
307
|
}
|
|
200
308
|
// Fallback: check by Symbol description (handles different Symbol instances)
|
|
@@ -220,26 +328,33 @@ 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
|
-
result[targetKey] = { [
|
|
342
|
+
result[targetKey] = { [_Op.eq]: val };
|
|
228
343
|
break;
|
|
229
344
|
case "ne":
|
|
230
|
-
result[targetKey] = { [
|
|
345
|
+
result[targetKey] = { [_Op.ne]: val };
|
|
231
346
|
break;
|
|
232
347
|
case "gt":
|
|
233
|
-
result[targetKey] = { [
|
|
348
|
+
result[targetKey] = { [_Op.gt]: val };
|
|
234
349
|
break;
|
|
235
350
|
case "gte":
|
|
236
|
-
result[targetKey] = { [
|
|
351
|
+
result[targetKey] = { [_Op.gte]: val };
|
|
237
352
|
break;
|
|
238
353
|
case "lt":
|
|
239
|
-
result[targetKey] = { [
|
|
354
|
+
result[targetKey] = { [_Op.lt]: val };
|
|
240
355
|
break;
|
|
241
356
|
case "lte":
|
|
242
|
-
result[targetKey] = { [
|
|
357
|
+
result[targetKey] = { [_Op.lte]: val };
|
|
243
358
|
break;
|
|
244
359
|
case "contains":
|
|
245
360
|
// Check if field is a date/datetime type
|
|
@@ -262,83 +377,88 @@ export class SequelizeModel extends AbstractModel {
|
|
|
262
377
|
}
|
|
263
378
|
else {
|
|
264
379
|
// Valid date - use exact match
|
|
265
|
-
result[targetKey] = { [
|
|
380
|
+
result[targetKey] = { [_Op.eq]: dateValue };
|
|
266
381
|
}
|
|
267
382
|
}
|
|
268
383
|
else {
|
|
269
384
|
// For time fields and non-date fields - use Op.like with wildcards
|
|
270
|
-
result[targetKey] = { [
|
|
385
|
+
result[targetKey] = { [_Op.like]: `%${val}%` };
|
|
271
386
|
}
|
|
272
387
|
break;
|
|
273
388
|
case "startsWith":
|
|
274
|
-
result[targetKey] = { [
|
|
389
|
+
result[targetKey] = { [_Op.startsWith]: val };
|
|
275
390
|
break;
|
|
276
391
|
case "endsWith":
|
|
277
|
-
result[targetKey] = { [
|
|
392
|
+
result[targetKey] = { [_Op.endsWith]: val };
|
|
278
393
|
break;
|
|
279
394
|
case ">":
|
|
280
|
-
result[targetKey] = { [
|
|
395
|
+
result[targetKey] = { [_Op.gt]: val };
|
|
281
396
|
break;
|
|
282
397
|
case ">=":
|
|
283
|
-
result[targetKey] = { [
|
|
398
|
+
result[targetKey] = { [_Op.gte]: val };
|
|
284
399
|
break;
|
|
285
400
|
case "<":
|
|
286
|
-
result[targetKey] = { [
|
|
401
|
+
result[targetKey] = { [_Op.lt]: val };
|
|
287
402
|
break;
|
|
288
403
|
case "<=":
|
|
289
|
-
result[targetKey] = { [
|
|
404
|
+
result[targetKey] = { [_Op.lte]: val };
|
|
290
405
|
break;
|
|
291
406
|
case "!=":
|
|
292
|
-
result[targetKey] = { [
|
|
407
|
+
result[targetKey] = { [_Op.ne]: val };
|
|
293
408
|
break;
|
|
294
409
|
case "in":
|
|
295
|
-
result[targetKey] = { [
|
|
410
|
+
result[targetKey] = { [_Op.in]: val };
|
|
296
411
|
break;
|
|
297
412
|
case "notIn":
|
|
298
|
-
result[targetKey] = { [
|
|
413
|
+
result[targetKey] = { [_Op.notIn]: val };
|
|
299
414
|
break;
|
|
300
415
|
case "nin":
|
|
301
|
-
result[targetKey] = { [
|
|
416
|
+
result[targetKey] = { [_Op.notIn]: val };
|
|
302
417
|
break;
|
|
303
418
|
case "between":
|
|
304
|
-
result[targetKey] = { [
|
|
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
|
-
result[targetKey] = { [
|
|
428
|
+
result[targetKey] = { [_Op.is]: null };
|
|
309
429
|
}
|
|
310
430
|
break;
|
|
311
431
|
case "isNotNull":
|
|
312
432
|
if (val) {
|
|
313
|
-
result[targetKey] = { [
|
|
433
|
+
result[targetKey] = { [_Op.not]: null };
|
|
314
434
|
}
|
|
315
435
|
break;
|
|
316
436
|
case "regex":
|
|
317
|
-
result[targetKey] = { [
|
|
437
|
+
result[targetKey] = { [_Op.regexp]: val };
|
|
318
438
|
break;
|
|
319
439
|
case "jsonContains": {
|
|
320
440
|
const values = Array.isArray(val) ? val : [val];
|
|
321
441
|
const clauses = values.map((item) => this._buildJsonContainsCondition(targetKey, item));
|
|
322
|
-
result[
|
|
323
|
-
...(result[
|
|
442
|
+
result[_Op.and] = [
|
|
443
|
+
...(result[_Op.and] ?? []),
|
|
324
444
|
...clauses
|
|
325
445
|
];
|
|
326
446
|
break;
|
|
327
447
|
}
|
|
328
448
|
case "$is":
|
|
329
|
-
result[targetKey] = { [
|
|
449
|
+
result[targetKey] = { [_Op.is]: val };
|
|
330
450
|
break;
|
|
331
451
|
case "$not":
|
|
332
|
-
result[targetKey] = { [
|
|
452
|
+
result[targetKey] = { [_Op.not]: val };
|
|
333
453
|
break;
|
|
334
454
|
case "$ne":
|
|
335
|
-
result[targetKey] = { [
|
|
455
|
+
result[targetKey] = { [_Op.ne]: val };
|
|
336
456
|
break;
|
|
337
457
|
case "not":
|
|
338
|
-
result[targetKey] = { [
|
|
458
|
+
result[targetKey] = { [_Op.not]: val };
|
|
339
459
|
break;
|
|
340
460
|
default:
|
|
341
|
-
result[targetKey] = { [
|
|
461
|
+
result[targetKey] = { [_Op.eq]: val };
|
|
342
462
|
}
|
|
343
463
|
}
|
|
344
464
|
}
|
|
@@ -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;
|
|
@@ -421,6 +541,7 @@ export class SequelizeModel extends AbstractModel {
|
|
|
421
541
|
}
|
|
422
542
|
// --- CREATE ---
|
|
423
543
|
async _create(data) {
|
|
544
|
+
await this._ensure();
|
|
424
545
|
// console.clear()
|
|
425
546
|
const assocNames = Object.keys(this.model.associations);
|
|
426
547
|
const plainData = {};
|
|
@@ -492,16 +613,19 @@ export class SequelizeModel extends AbstractModel {
|
|
|
492
613
|
}
|
|
493
614
|
// --- FIND ONE ---
|
|
494
615
|
async _findOne(criteria) {
|
|
616
|
+
await this._ensure();
|
|
495
617
|
// console.debug(">> _findOne: input criteria:", criteria);
|
|
496
618
|
const { where, attributes } = this._convertAdminizerCriteriaToSequelizeOptions(criteria);
|
|
497
619
|
const includes = criteria.populate
|
|
498
620
|
? this._buildCriteriaIncludes(criteria.populate)
|
|
499
621
|
: this._buildIncludes();
|
|
622
|
+
this._applyPopulateOn(includes, criteria.populateOn);
|
|
623
|
+
const selectedAttributes = this._ensureForeignKeysSelected(attributes, criteria.populateOn);
|
|
500
624
|
// console.debug(">> _findOne: converted where:", where);
|
|
501
625
|
// console.debug(">> _findOne: includes:", includes);
|
|
502
626
|
let instance = null;
|
|
503
627
|
try {
|
|
504
|
-
instance = await this.model.findOne({ where, attributes, include: includes });
|
|
628
|
+
instance = await this.model.findOne({ where, attributes: selectedAttributes, include: includes });
|
|
505
629
|
// console.debug(">> _findOne: raw instance:", instance ? instance.toJSON() : null);
|
|
506
630
|
}
|
|
507
631
|
catch (err) {
|
|
@@ -513,11 +637,13 @@ export class SequelizeModel extends AbstractModel {
|
|
|
513
637
|
return null;
|
|
514
638
|
}
|
|
515
639
|
const plain = instance.get({ plain: true });
|
|
640
|
+
this._collapsePopulateOn([plain], criteria.populateOn);
|
|
516
641
|
// console.debug(">> _findOne: plain result:", plain);
|
|
517
642
|
return plain;
|
|
518
643
|
}
|
|
519
644
|
// --- FIND MANY ---
|
|
520
645
|
async _find(criteria = {}) {
|
|
646
|
+
await this._ensure();
|
|
521
647
|
const assocNames = Object.keys(this.model.associations);
|
|
522
648
|
// console.debug(">> _find: input criteria:", criteria, "options:", options);
|
|
523
649
|
const { where, limit, offset, order, attributes } = this._convertAdminizerCriteriaToSequelizeOptions(criteria);
|
|
@@ -528,6 +654,8 @@ export class SequelizeModel extends AbstractModel {
|
|
|
528
654
|
const includes = criteria.populate
|
|
529
655
|
? this._buildCriteriaIncludes(criteria.populate)
|
|
530
656
|
: assocNames.map((alias) => this._buildListInclude(alias, usedRelationAliases, hasRelationPathCondition));
|
|
657
|
+
this._applyPopulateOn(includes, criteria.populateOn);
|
|
658
|
+
const selectedAttributes = this._ensureForeignKeysSelected(attributes, criteria.populateOn);
|
|
531
659
|
// console.debug(">> _find: where, limit, offset, order, includes:", {
|
|
532
660
|
// where,
|
|
533
661
|
// limit,
|
|
@@ -538,33 +666,18 @@ export class SequelizeModel extends AbstractModel {
|
|
|
538
666
|
let instances;
|
|
539
667
|
instances = await this.model.findAll({
|
|
540
668
|
where,
|
|
541
|
-
attributes,
|
|
669
|
+
attributes: selectedAttributes,
|
|
542
670
|
limit,
|
|
543
671
|
offset,
|
|
544
672
|
order,
|
|
545
673
|
include: includes,
|
|
546
674
|
subQuery: hasRelationPathCondition ? false : undefined
|
|
547
675
|
});
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
// @ts-ignore accessors is present
|
|
552
|
-
const getAccessor = this.model.associations[alias].accessors.get;
|
|
553
|
-
if (typeof inst[getAccessor] === "function") {
|
|
554
|
-
try {
|
|
555
|
-
const related = await inst[getAccessor]();
|
|
556
|
-
const mapped = Array.isArray(related)
|
|
557
|
-
? related.map((r) => r.toJSON())
|
|
558
|
-
: related?.toJSON();
|
|
559
|
-
// console.debug(`---- get${alias}():`, mapped);
|
|
560
|
-
}
|
|
561
|
-
catch (e) {
|
|
562
|
-
// console.error(`!! error when calling ${getAccessor}():`, e);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
}
|
|
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.
|
|
567
679
|
const plain = instances.map(i => i.get({ plain: true }));
|
|
680
|
+
this._collapsePopulateOn(plain, criteria.populateOn);
|
|
568
681
|
// console.debug(">> _find: plain results:", plain);
|
|
569
682
|
return plain;
|
|
570
683
|
}
|
|
@@ -573,6 +686,7 @@ export class SequelizeModel extends AbstractModel {
|
|
|
573
686
|
* Bypasses Adminizer criteria conversion for direct Sequelize usage
|
|
574
687
|
*/
|
|
575
688
|
async findWithRawWhere(where, options = {}) {
|
|
689
|
+
await this._ensure();
|
|
576
690
|
const assocNames = Object.keys(this.model.associations);
|
|
577
691
|
const includes = options.populate !== false
|
|
578
692
|
? assocNames.map(a => ({ association: a }))
|
|
@@ -590,6 +704,7 @@ export class SequelizeModel extends AbstractModel {
|
|
|
590
704
|
* Public method for count with raw SQL where clauses
|
|
591
705
|
*/
|
|
592
706
|
async countWithRawWhere(where) {
|
|
707
|
+
await this._ensure();
|
|
593
708
|
const assocNames = Object.keys(this.model.associations);
|
|
594
709
|
const include = assocNames.map((association) => ({ association }));
|
|
595
710
|
return await this.model.count({
|
|
@@ -601,6 +716,7 @@ export class SequelizeModel extends AbstractModel {
|
|
|
601
716
|
}
|
|
602
717
|
// --- UPDATE ONE ---
|
|
603
718
|
async _updateOne(criteria, data) {
|
|
719
|
+
await this._ensure();
|
|
604
720
|
const { where } = this._convertAdminizerCriteriaToSequelizeOptions(criteria);
|
|
605
721
|
const record = await this.model.findOne({ where });
|
|
606
722
|
if (!record)
|
|
@@ -632,6 +748,7 @@ export class SequelizeModel extends AbstractModel {
|
|
|
632
748
|
}
|
|
633
749
|
// --- UPDATE MANY ---
|
|
634
750
|
async _update(criteria, data) {
|
|
751
|
+
await this._ensure();
|
|
635
752
|
const { where } = this._convertAdminizerCriteriaToSequelizeOptions(criteria);
|
|
636
753
|
const assocNames = Object.keys(this.model.associations);
|
|
637
754
|
const plainData = {};
|
|
@@ -664,74 +781,68 @@ export class SequelizeModel extends AbstractModel {
|
|
|
664
781
|
});
|
|
665
782
|
return reloaded.map((r) => r.get({ plain: true }));
|
|
666
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
|
+
}
|
|
667
828
|
// --- DESTROY ONE ---
|
|
668
829
|
async _destroyOne(criteria) {
|
|
830
|
+
await this._ensure();
|
|
669
831
|
const { where } = this._convertAdminizerCriteriaToSequelizeOptions(criteria);
|
|
670
832
|
const record = await this.model.findOne({ where });
|
|
671
833
|
if (!record)
|
|
672
834
|
return null;
|
|
673
|
-
|
|
674
|
-
for (const alias of assocNames) {
|
|
675
|
-
const assoc = this.model.associations[alias];
|
|
676
|
-
// @ts-ignore: accessors should exist
|
|
677
|
-
const getAccessor = assoc.accessors?.get;
|
|
678
|
-
if (typeof record[getAccessor] === "function") {
|
|
679
|
-
try {
|
|
680
|
-
const related = await record[getAccessor]();
|
|
681
|
-
// 🧹Deleting related posts
|
|
682
|
-
if (Array.isArray(related)) {
|
|
683
|
-
for (const r of related) {
|
|
684
|
-
if (typeof r.destroy === "function") {
|
|
685
|
-
await r.destroy();
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
else if (related && typeof related.destroy === "function") {
|
|
690
|
-
await related.destroy();
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
catch (e) {
|
|
694
|
-
// console.warn(`Failed to fetch/delete relation "${alias}":`, e);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
}
|
|
835
|
+
await this._cascadeDelete([record.get(this.model.primaryKeyAttribute)]);
|
|
698
836
|
const raw = record.get({ plain: true });
|
|
699
837
|
await record.destroy();
|
|
700
838
|
return raw;
|
|
701
839
|
}
|
|
702
840
|
// --- DESTROY MANY ---
|
|
703
841
|
async _destroy(criteria) {
|
|
842
|
+
await this._ensure();
|
|
704
843
|
const { where } = this._convertAdminizerCriteriaToSequelizeOptions(criteria);
|
|
705
844
|
const records = await this.model.findAll({ where });
|
|
706
|
-
|
|
707
|
-
for (const record of records) {
|
|
708
|
-
for (const alias of assocNames) {
|
|
709
|
-
const assoc = this.model.associations[alias];
|
|
710
|
-
// 🛑 We don’t touch parental ties
|
|
711
|
-
if (assoc.associationType === "BelongsTo")
|
|
712
|
-
continue;
|
|
713
|
-
// @ts-ignore accessor exists
|
|
714
|
-
const getAccessor = assoc.accessors?.get;
|
|
715
|
-
if (typeof record[getAccessor] === "function") {
|
|
716
|
-
try {
|
|
717
|
-
const related = await record[getAccessor]();
|
|
718
|
-
if (Array.isArray(related)) {
|
|
719
|
-
for (const rel of related) {
|
|
720
|
-
if (typeof rel.destroy === "function") {
|
|
721
|
-
await rel.destroy();
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
else if (related && typeof related.destroy === "function") {
|
|
726
|
-
await related.destroy();
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
catch (e) {
|
|
730
|
-
// console.warn(`Failed to delete relation ${alias}:`, e);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
}
|
|
845
|
+
await this._cascadeDelete(records.map((r) => r.get(this.model.primaryKeyAttribute)));
|
|
735
846
|
const raw = records.map((r) => r.get({ plain: true }));
|
|
736
847
|
await this.model.destroy({ where });
|
|
737
848
|
return raw;
|
|
@@ -805,7 +916,7 @@ export class SequelizeModel extends AbstractModel {
|
|
|
805
916
|
}
|
|
806
917
|
const association = this.model.associations[alias];
|
|
807
918
|
const isUsedInWhere = usedRelationAliases.has(alias);
|
|
808
|
-
const isHasMany = association instanceof
|
|
919
|
+
const isHasMany = association instanceof _HasMany || association?.associationType === 'HasMany';
|
|
809
920
|
// When filtering by relation path ($alias.field$), extra joined hasMany associations
|
|
810
921
|
// can duplicate base rows and break page size. Load such associations separately.
|
|
811
922
|
if (isHasMany && !isUsedInWhere) {
|
|
@@ -816,6 +927,70 @@ export class SequelizeModel extends AbstractModel {
|
|
|
816
927
|
_buildIncludes() {
|
|
817
928
|
return Object.keys(this.model.associations).map(key => ({ association: key }));
|
|
818
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
|
+
}
|
|
819
994
|
_buildAttributes(select, rawAttributes = this.model.rawAttributes) {
|
|
820
995
|
if (!select) {
|
|
821
996
|
return undefined;
|
|
@@ -861,10 +1036,14 @@ export class SequelizeAdapter extends AbstractAdapter {
|
|
|
861
1036
|
sequelize;
|
|
862
1037
|
Model = SequelizeModel;
|
|
863
1038
|
constructor(sequelize, options = {}) {
|
|
1039
|
+
ensureSequelize();
|
|
864
1040
|
super("sequelize", sequelize, options);
|
|
1041
|
+
// Note: ensureSequelize is fire-and-forget during construction
|
|
1042
|
+
// Methods also call this._ensure() to guarantee initialization
|
|
865
1043
|
this.sequelize = sequelize;
|
|
866
1044
|
}
|
|
867
1045
|
get models() {
|
|
1046
|
+
``;
|
|
868
1047
|
return this.sequelize.models;
|
|
869
1048
|
}
|
|
870
1049
|
getModel(modelName) {
|