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.
- package/assets/abnfDiagram-VCTEODGH-BTgLIXrb.js +1 -0
- package/assets/add-C8V8qjvm.js +1 -0
- package/assets/add-group-Ezq_Ocot.js +1 -0
- package/assets/add-user-CluNf2QG.js +1 -0
- package/assets/ai-assistant/agent.es.js +28464 -0
- package/assets/app-C96pcs-J.js +138 -0
- package/assets/app-DPmI-MvN.css +1 -0
- package/assets/arc-G6n0TtRf.js +1 -0
- package/assets/architectureDiagram-5GKGNRK7-TQ4_iSAG.js +36 -0
- package/assets/asmcrypto-DZeQH0LC.js +1 -0
- package/assets/blockDiagram-I7D4REHJ-1q050zQd.js +129 -0
- package/assets/c4Diagram-7LVT6UL2-BXn0hW62.js +38 -0
- package/assets/{catalog-dk6a0I1u.js → catalog-Cpx4OH_r.js} +3 -3
- package/assets/channel-JZzXP_E5.js +1 -0
- package/assets/chunk-2Q5K7J3B-BSnocdBz.js +1 -0
- package/assets/chunk-5VM5RSS4-BvfoMSpq.js +15 -0
- package/assets/chunk-F27PBJKO-CMyFcBlX.js +1 -0
- package/assets/chunk-IMKFNOWR--8Y02hwt.js +231 -0
- package/assets/chunk-JWPE2WC7-C18m6v8d.js +1 -0
- package/assets/chunk-POPQ4Y6H-DzoTZe0a.js +1 -0
- package/assets/chunk-SVP7TREG-Ce98IXh7.js +88 -0
- package/assets/chunk-TICWLB2K-BeLoUlaL.js +206 -0
- package/assets/chunk-XXDRQBXY-DCZSZ1aW.js +1 -0
- package/assets/classDiagram-ZZMXUADV-jA4zeKxs.js +1 -0
- package/assets/classDiagram-v2-VYDZK3BY-jA4zeKxs.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-Cy9np608.js +1 -0
- package/assets/cynefin-OW5HDTMX-Ba6uP79R.js +166 -0
- package/assets/cynefinDiagram-5FMLGOSQ-DhMBBq7T.js +62 -0
- package/assets/cytoscape.esm-DzSz-X2X.js +321 -0
- package/assets/dagre-GXQ25YYZ-BdAnQUCV.js +4 -0
- package/assets/dashboard-BFVFBIio.js +3 -0
- package/assets/debounce-B3mXI_sx.js +1 -0
- package/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/assets/diagram-S7CK7UJ4-BDYUGbNl.js +30 -0
- package/assets/diagram-UQ7AKVKN-PURK0-X-.js +41 -0
- package/assets/diagram-VSXAHHWV-DxHdFxl9.js +3 -0
- package/assets/diagram-VX7I27RA-Vz8xmeYc.js +24 -0
- package/assets/diagram-Z3DM3KII-CFXUpBMy.js +24 -0
- package/assets/docs-DU3h-HtS.js +1 -0
- package/assets/ebnfDiagram-PWID7BFC-BXNvxMGP.js +1 -0
- package/assets/erDiagram-RLTQ6QDP-DN0GgZEI.js +99 -0
- package/assets/flowDiagram-HODETNUW-DXdnwgBv.js +168 -0
- package/assets/ganttDiagram-EL5Y4UJY-Cv8_dxX3.js +292 -0
- package/assets/gitGraphDiagram-WWUBYQGX-BO8T858E.js +106 -0
- package/assets/history-BmwWpckq.js +1 -0
- package/assets/infoDiagram-27XIBGKW-DMSJ1RNc.js +2 -0
- package/assets/init-Gi6I4Gst.js +1 -0
- package/assets/init-user-BIdB8U_4.js +1 -0
- package/assets/isObject-DL-BuaBl.js +1 -0
- package/assets/ishikawaDiagram-5VMMS53U-_tLJESbl.js +70 -0
- package/assets/journeyDiagram-3NMN7TZE-DMMhdo-_.js +139 -0
- package/assets/kanban-definition-UXKFOSKX-CxQrB4t6.js +89 -0
- package/assets/katex-HP8lGamR.js +257 -0
- package/assets/layout-_oS_xxCJ.js +1 -0
- package/assets/linear-VLYuNK3x.js +1 -0
- package/assets/{list-CcMtS4hD.js → list-YTyJQtiR.js} +2 -2
- package/assets/login-B9GlhjZh.js +1 -0
- package/assets/manifest.json +834 -58
- package/assets/media-manager-Bb1VWlzd.js +6 -0
- package/assets/mermaid.core-BknV2vft.js +310 -0
- package/assets/mindmap-definition-YA3MSWOX-DBjjjDq4.js +96 -0
- package/assets/module-DQ1NK8Xg.js +1 -0
- package/assets/notification-CgNOsXvn.js +20 -0
- package/assets/ordinal-Cboi1Yqb.js +1 -0
- package/assets/pegDiagram-XKGWAZYB-uRznTaO4.js +1 -0
- package/assets/pieDiagram-E7YTZNPT-D1TkCH8I.js +39 -0
- package/assets/quadrantDiagram-AXDQQJYC-BofJ36Jz.js +7 -0
- package/assets/railroadDiagram-O6MQD6OU-e1ZM2IT1.js +1 -0
- package/assets/register-DjTtMayF.js +1 -0
- package/assets/requirementDiagram-BXWQKSXE-DQWArRbb.js +84 -0
- package/assets/ru-CqAtka69.js +1 -0
- package/assets/sankeyDiagram-P5KCCOFB-fVOu2jzi.js +40 -0
- package/assets/sequenceDiagram-WJ2MYXX4-Dj24cqpu.js +162 -0
- package/assets/sizeCapture-INFHLROL-CsApVrVi.js +1 -0
- package/assets/stateDiagram-D77RDMKH-IU2Szlid.js +1 -0
- package/assets/stateDiagram-v2-MP3YSRHH-B-EW3cIC.js +1 -0
- package/assets/swimlanes-42K2YHIH-97rBqTR-.js +1 -0
- package/assets/swimlanesDiagram-VR7AAH4N-CqG2iydK.js +8 -0
- package/assets/timeline-definition-24CTP7MA-DSgRzx1V.js +120 -0
- package/assets/toNumber-DZb-r9sy.js +1 -0
- package/assets/use-appearance-CXz2xWgT.js +1 -0
- package/assets/user-filters-list-BAR4CBN_.js +1 -0
- package/assets/vennDiagram-4TSXK5OY-BqMksFxE.js +34 -0
- package/assets/wardleyDiagram-VM6X3IG4-BMmxRODC.js +78 -0
- package/assets/welcome-CPXT7_4b.js +1 -0
- package/assets/with-app-layout-C6dqetfx.js +31 -0
- package/assets/xychartDiagram-S5SC5T6Z-3xRuF0l4.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 +63 -2
- package/helpers/accessRightsHelper.js +202 -13
- 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 +30 -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 +40 -1
- package/lib/app-manager/AppManager.js +48 -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 +61 -2
- package/lib/model/ModelHandler.js +136 -8
- package/lib/model/adapter/sequelize.d.ts +40 -1
- package/lib/model/adapter/sequelize.js +243 -77
- 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 +9 -0
- package/system/buildInternalModelAccess.js +37 -4
- package/system/defaults.js +7 -1
- package/system/validateSystemModels.js +29 -9
- package/assets/add-CBlLpLr0.js +0 -1
- package/assets/add-group-H0IKIsNm.js +0 -1
- package/assets/add-user-CoTLmxtF.js +0 -1
- package/assets/app-DNXiApCk.css +0 -1
- package/assets/app-layout-xxJpLAvb.js +0 -1
- package/assets/app.js +0 -138
- package/assets/dashboard-DVJS5EPP.js +0 -3
- package/assets/debounce-BMmXVQ06.js +0 -1
- package/assets/history-bWRuzbye.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-CqO_BKsH.js +0 -1
- package/assets/notification-B5l2wwmi.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-C7kMKN5f.js +0 -1
- 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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]) =>
|
|
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
|
-
|
|
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
|
-
|
|
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);
|