@open-rap/runtime 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +14 -2
- package/dist/abap/background.d.ts +14 -0
- package/dist/abap/background.js +130 -0
- package/dist/abap/background.js.map +1 -0
- package/dist/abap/bridge.d.ts +62 -0
- package/dist/abap/bridge.js +402 -0
- package/dist/abap/bridge.js.map +1 -0
- package/dist/abap/convert.d.ts +26 -0
- package/dist/abap/convert.js +197 -0
- package/dist/abap/convert.js.map +1 -0
- package/dist/abap/http.d.ts +34 -0
- package/dist/abap/http.js +133 -0
- package/dist/abap/http.js.map +1 -0
- package/dist/abap/mail.d.ts +10 -0
- package/dist/abap/mail.js +83 -0
- package/dist/abap/mail.js.map +1 -0
- package/dist/abap/odata-client.d.ts +33 -0
- package/dist/abap/odata-client.js +576 -0
- package/dist/abap/odata-client.js.map +1 -0
- package/dist/abap/sql.d.ts +92 -0
- package/dist/abap/sql.js +261 -0
- package/dist/abap/sql.js.map +1 -0
- package/dist/abap/utils.d.ts +41 -0
- package/dist/abap/utils.js +976 -0
- package/dist/abap/utils.js.map +1 -0
- package/dist/app.d.ts +53 -0
- package/dist/app.js +169 -0
- package/dist/app.js.map +1 -0
- package/dist/auth/auth.d.ts +68 -0
- package/dist/auth/auth.js +140 -0
- package/dist/auth/auth.js.map +1 -0
- package/dist/auth/authorizations.d.ts +39 -0
- package/dist/auth/authorizations.js +103 -0
- package/dist/auth/authorizations.js.map +1 -0
- package/dist/auth/business-users.d.ts +15 -0
- package/dist/auth/business-users.js +36 -0
- package/dist/auth/business-users.js.map +1 -0
- package/dist/auth/dev-oauth.d.ts +38 -0
- package/dist/auth/dev-oauth.js +156 -0
- package/dist/auth/dev-oauth.js.map +1 -0
- package/dist/auth/environment.d.ts +7 -0
- package/dist/auth/environment.js +57 -0
- package/dist/auth/environment.js.map +1 -0
- package/dist/auth/session.d.ts +45 -0
- package/dist/auth/session.js +257 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/auth/user.d.ts +21 -0
- package/dist/auth/user.js +12 -0
- package/dist/auth/user.js.map +1 -0
- package/dist/db/database.d.ts +30 -0
- package/dist/db/database.js +131 -0
- package/dist/db/database.js.map +1 -0
- package/dist/db/seed.d.ts +9 -0
- package/dist/db/seed.js +63 -0
- package/dist/db/seed.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/launchpad/dev.d.ts +14 -0
- package/dist/launchpad/dev.js +48 -0
- package/dist/launchpad/dev.js.map +1 -0
- package/dist/launchpad/proxy.d.ts +30 -0
- package/dist/launchpad/proxy.js +98 -0
- package/dist/launchpad/proxy.js.map +1 -0
- package/dist/launchpad/server.d.ts +77 -0
- package/dist/launchpad/server.js +177 -0
- package/dist/launchpad/server.js.map +1 -0
- package/dist/mcp/server.d.ts +22 -0
- package/dist/mcp/server.js +222 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/odata/access.d.ts +31 -0
- package/dist/odata/access.js +142 -0
- package/dist/odata/access.js.map +1 -0
- package/dist/odata/batch.d.ts +36 -0
- package/dist/odata/batch.js +147 -0
- package/dist/odata/batch.js.map +1 -0
- package/dist/odata/csrf.d.ts +13 -0
- package/dist/odata/csrf.js +27 -0
- package/dist/odata/csrf.js.map +1 -0
- package/dist/odata/custom.d.ts +90 -0
- package/dist/odata/custom.js +373 -0
- package/dist/odata/custom.js.map +1 -0
- package/dist/odata/draft.d.ts +78 -0
- package/dist/odata/draft.js +590 -0
- package/dist/odata/draft.js.map +1 -0
- package/dist/odata/errors.d.ts +27 -0
- package/dist/odata/errors.js +41 -0
- package/dist/odata/errors.js.map +1 -0
- package/dist/odata/handler.d.ts +90 -0
- package/dist/odata/handler.js +312 -0
- package/dist/odata/handler.js.map +1 -0
- package/dist/odata/json.d.ts +23 -0
- package/dist/odata/json.js +85 -0
- package/dist/odata/json.js.map +1 -0
- package/dist/odata/read.d.ts +90 -0
- package/dist/odata/read.js +686 -0
- package/dist/odata/read.js.map +1 -0
- package/dist/odata/service.d.ts +45 -0
- package/dist/odata/service.js +145 -0
- package/dist/odata/service.js.map +1 -0
- package/dist/odata/uri/ast.d.ts +103 -0
- package/dist/odata/uri/ast.js +3 -0
- package/dist/odata/uri/ast.js.map +1 -0
- package/dist/odata/uri/parser.d.ts +26 -0
- package/dist/odata/uri/parser.js +697 -0
- package/dist/odata/uri/parser.js.map +1 -0
- package/dist/odata/write.d.ts +127 -0
- package/dist/odata/write.js +799 -0
- package/dist/odata/write.js.map +1 -0
- package/dist/platform.d.ts +14 -0
- package/dist/platform.js +66 -0
- package/dist/platform.js.map +1 -0
- package/dist/rap/bo.d.ts +76 -0
- package/dist/rap/bo.js +184 -0
- package/dist/rap/bo.js.map +1 -0
- package/dist/rap/kernel.d.ts +7 -0
- package/dist/rap/kernel.js +287 -0
- package/dist/rap/kernel.js.map +1 -0
- package/dist/rap/runtime.d.ts +84 -0
- package/dist/rap/runtime.js +128 -0
- package/dist/rap/runtime.js.map +1 -0
- package/dist/rap/transaction.d.ts +227 -0
- package/dist/rap/transaction.js +1385 -0
- package/dist/rap/transaction.js.map +1 -0
- package/dist/rap/types.d.ts +74 -0
- package/dist/rap/types.js +25 -0
- package/dist/rap/types.js.map +1 -0
- package/dist/server.d.ts +34 -0
- package/dist/server.js +114 -0
- package/dist/server.js.map +1 -0
- package/dist/ui/plugin.d.ts +14 -0
- package/dist/ui/plugin.js +47 -0
- package/dist/ui/plugin.js.map +1 -0
- package/dist/ui/resources.d.ts +22 -0
- package/dist/ui/resources.js +88 -0
- package/dist/ui/resources.js.map +1 -0
- package/dist/ui/variants.d.ts +25 -0
- package/dist/ui/variants.js +68 -0
- package/dist/ui/variants.js.map +1 -0
- package/dist/values.d.ts +43 -0
- package/dist/values.js +367 -0
- package/dist/values.js.map +1 -0
- package/package.json +60 -3
|
@@ -0,0 +1,1385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The managed RAP transaction (LUW): transactional buffer over the persistent tables, the
|
|
3
|
+
* operations of the business objects with their checks (authorization, feature control, locks,
|
|
4
|
+
* numbering, administrative fields), the determinations on modify (decision D21) and the save
|
|
5
|
+
* sequence (determinations on save, validations, write).
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { sql } from "kysely";
|
|
9
|
+
import { abapName, derivedType, getAnnotation, same } from "@open-rap/model";
|
|
10
|
+
import { appendRow, component, isFlagOn, rowsOf, setFlag } from "../abap/convert.js";
|
|
11
|
+
import { withSavepoint } from "../db/database.js";
|
|
12
|
+
import { fromSql, initialValue, isInitial, toSql } from "../values.js";
|
|
13
|
+
import { keyOf, keyString } from "./bo.js";
|
|
14
|
+
import { RapRuntimeError, Responses } from "./types.js";
|
|
15
|
+
const MAX_ROUNDS = 20;
|
|
16
|
+
export class Transaction {
|
|
17
|
+
services;
|
|
18
|
+
db;
|
|
19
|
+
user;
|
|
20
|
+
instances = new Map();
|
|
21
|
+
cids = new Map();
|
|
22
|
+
events = [];
|
|
23
|
+
seq = 0;
|
|
24
|
+
seen = new Map();
|
|
25
|
+
locked = new Set();
|
|
26
|
+
source;
|
|
27
|
+
saved = false;
|
|
28
|
+
/** ETag masters whose ETag field changes with their `etag dependent` instances (D32). */
|
|
29
|
+
etagTouched = new Set();
|
|
30
|
+
/** The saver runs (save_modified, cleanup): the only place where ABAP SQL may write. */
|
|
31
|
+
inSaver = false;
|
|
32
|
+
constructor(services, db, user) {
|
|
33
|
+
this.services = services;
|
|
34
|
+
this.db = db;
|
|
35
|
+
this.user = user;
|
|
36
|
+
}
|
|
37
|
+
get bo() {
|
|
38
|
+
return this.services.bo;
|
|
39
|
+
}
|
|
40
|
+
get bridge() {
|
|
41
|
+
return this.services.bridge;
|
|
42
|
+
}
|
|
43
|
+
// -------------------------------------------------------------------------------------------
|
|
44
|
+
// buffer
|
|
45
|
+
id(bo, key) {
|
|
46
|
+
return `${bo.entity.name.toUpperCase()}|${keyString(bo, key)}`;
|
|
47
|
+
}
|
|
48
|
+
/** The instances of a custom entity meeting equalities, read by its query class (D43). */
|
|
49
|
+
async readCustom(bo, equalities) {
|
|
50
|
+
if (this.services.customRead === undefined) {
|
|
51
|
+
throw new RapRuntimeError(`${bo.entity.name}: custom entities need their query class`);
|
|
52
|
+
}
|
|
53
|
+
const rows = await this.services.customRead(bo.entity, equalities);
|
|
54
|
+
return rows.map((record) => Object.fromEntries(bo.fields.map((f) => [f.name, record[f.name] ?? initialValue(f.type)])));
|
|
55
|
+
}
|
|
56
|
+
async load(bo, key) {
|
|
57
|
+
if (bo.entity.kind === "custom") {
|
|
58
|
+
return (await this.readCustom(bo, bo.keys.map((k) => ({ name: k.name, value: (key[k.name] ?? null) }))))[0];
|
|
59
|
+
}
|
|
60
|
+
const conditions = bo.keys.map((k) => sql `${sql.id(bo.columns.get(k.name) ?? abapName(k.name))} = ${toSql(k.type, key[k.name] ?? null)}`);
|
|
61
|
+
const result = await sql `SELECT * FROM ${sql.id(bo.table.name)} WHERE ${sql.join(conditions, sql ` AND `)}`.execute(this.db);
|
|
62
|
+
const record = result.rows[0];
|
|
63
|
+
return record === undefined ? undefined : (await this.withCalculated(bo, [this.fromRecord(bo, record)]))[0];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The calculated elements (D67) of instances read from the persistent table, read from the CDS
|
|
67
|
+
* view: their values as of the last save.
|
|
68
|
+
*/
|
|
69
|
+
async withCalculated(bo, rows) {
|
|
70
|
+
const calculated = bo.fields.filter((f) => f.expression !== undefined);
|
|
71
|
+
const view = bo.entity.sqlView;
|
|
72
|
+
if (calculated.length === 0 || view === undefined || same(view, bo.table.name) || rows.length === 0) {
|
|
73
|
+
return rows;
|
|
74
|
+
}
|
|
75
|
+
const byKey = (row) => sql `(${sql.join(bo.keys.map((k) => sql `${sql.id(k.name)} = ${toSql(k.type, row[k.name] ?? null)}`), sql ` AND `)})`;
|
|
76
|
+
const result = await sql `SELECT ${sql.join([...bo.keys, ...calculated].map((f) => sql.id(f.name)))} FROM ${sql.id(view)} WHERE ${sql.join(rows.map(byKey), sql ` OR `)}`.execute(this.db);
|
|
77
|
+
const records = new Map(result.rows.map((record) => [keyString(bo, Object.fromEntries(bo.keys.map((k) => [k.name, fromSql(k.type, record[k.name])]))), record]));
|
|
78
|
+
for (const row of rows) {
|
|
79
|
+
const record = records.get(keyString(bo, row));
|
|
80
|
+
for (const field of calculated) {
|
|
81
|
+
row[field.name] = record === undefined ? initialValue(field.type) : fromSql(field.type, record[field.name]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return rows;
|
|
85
|
+
}
|
|
86
|
+
fromRecord(bo, record) {
|
|
87
|
+
const row = {};
|
|
88
|
+
for (const field of bo.fields) {
|
|
89
|
+
const column = bo.columns.get(field.name);
|
|
90
|
+
row[field.name] = column === undefined ? initialValue(field.type) : fromSql(field.type, record[column]);
|
|
91
|
+
}
|
|
92
|
+
return row;
|
|
93
|
+
}
|
|
94
|
+
/** The instance with this key, from the buffer or the database; undefined if it does not exist. */
|
|
95
|
+
async instance(bo, key) {
|
|
96
|
+
const id = this.id(bo, key);
|
|
97
|
+
const buffered = this.instances.get(id);
|
|
98
|
+
if (buffered !== undefined) {
|
|
99
|
+
return buffered.deleted ? undefined : buffered;
|
|
100
|
+
}
|
|
101
|
+
const row = await this.load(bo, key);
|
|
102
|
+
if (row === undefined) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const instance = { bo, row, persisted: { ...row }, deleted: false, changed: new Set() };
|
|
106
|
+
this.instances.set(id, instance);
|
|
107
|
+
return instance;
|
|
108
|
+
}
|
|
109
|
+
/** Key of a request row: its key, or the instance created with its `%cid_ref`. */
|
|
110
|
+
resolveKey(bo, key, cidRef) {
|
|
111
|
+
if (cidRef !== undefined && cidRef !== "") {
|
|
112
|
+
const created = this.cids.get(cidRef);
|
|
113
|
+
if (created !== undefined && created.bo === bo) {
|
|
114
|
+
return created.key;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (key === undefined || bo.keys.every((k) => isInitial(k.type, key[k.name]))) {
|
|
118
|
+
return cidRef === undefined || cidRef === "" ? key : undefined;
|
|
119
|
+
}
|
|
120
|
+
return keyOf(bo, key);
|
|
121
|
+
}
|
|
122
|
+
record(bo, key, kind, fields) {
|
|
123
|
+
const event = { seq: ++this.seq, bo, key: { ...key }, kind, fields };
|
|
124
|
+
if (this.source !== undefined) {
|
|
125
|
+
event.source = this.source;
|
|
126
|
+
}
|
|
127
|
+
this.events.push(event);
|
|
128
|
+
}
|
|
129
|
+
/** Instances of the buffer and the database reached by an association. */
|
|
130
|
+
async associated(bo, association, source) {
|
|
131
|
+
const target = association.target;
|
|
132
|
+
const conditions = association.on.map((pair) => {
|
|
133
|
+
const field = target.fields.find((f) => same(f.name, pair.target));
|
|
134
|
+
return { field, value: source[bo.fields.find((f) => same(f.name, pair.source))?.name ?? pair.source] ?? null };
|
|
135
|
+
});
|
|
136
|
+
const read = target.entity.kind === "custom"
|
|
137
|
+
? await this.readCustom(target, conditions.map(({ field, value }) => ({ name: field.name, value: value })))
|
|
138
|
+
: await this.withCalculated(target, (await sql `SELECT * FROM ${sql.id(target.table.name)} WHERE ${sql.join(conditions.map(({ field, value }) => sql `${sql.id(target.columns.get(field.name) ?? abapName(field.name))} = ${toSql(field.type, value)}`), sql ` AND `)}`.execute(this.db)).rows.map((record) => this.fromRecord(target, record)));
|
|
139
|
+
const rows = new Map();
|
|
140
|
+
for (const row of read) {
|
|
141
|
+
const id = this.id(target, row);
|
|
142
|
+
const buffered = this.instances.get(id);
|
|
143
|
+
if (buffered === undefined) {
|
|
144
|
+
this.instances.set(id, { bo: target, row, persisted: { ...row }, deleted: false, changed: new Set() });
|
|
145
|
+
rows.set(id, row);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (const [id, instance] of this.instances) {
|
|
149
|
+
if (instance.bo !== target || instance.deleted) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const matches = conditions.every(({ field, value }) => instance.row[field.name] === value);
|
|
153
|
+
if (matches) {
|
|
154
|
+
rows.set(id, instance.row);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
rows.delete(id);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return [...rows.values()];
|
|
161
|
+
}
|
|
162
|
+
// -------------------------------------------------------------------------------------------
|
|
163
|
+
// checks
|
|
164
|
+
failure(responses, bo, failure, operation) {
|
|
165
|
+
const entry = { entity: bo.entity.name, cause: failure.cause };
|
|
166
|
+
if (failure.key !== undefined) {
|
|
167
|
+
entry.key = failure.key;
|
|
168
|
+
}
|
|
169
|
+
if (failure.cid !== undefined) {
|
|
170
|
+
entry.cid = failure.cid;
|
|
171
|
+
}
|
|
172
|
+
if (operation !== undefined) {
|
|
173
|
+
entry.operation = operation;
|
|
174
|
+
}
|
|
175
|
+
responses.failed.push(entry);
|
|
176
|
+
}
|
|
177
|
+
/** Locks the lock master of an existing instance (`SELECT … FOR UPDATE NOWAIT`). */
|
|
178
|
+
async lock(bo, key) {
|
|
179
|
+
let master = bo;
|
|
180
|
+
let masterKey = key;
|
|
181
|
+
for (let depth = 0; depth < 20 && this.bo.lockMaster(master) !== master; depth++) {
|
|
182
|
+
const parent = master.parent;
|
|
183
|
+
if (parent === undefined) {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
const instance = await this.instance(master, masterKey);
|
|
187
|
+
if (instance === undefined) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
const parentKey = {};
|
|
191
|
+
for (const pair of parent.on) {
|
|
192
|
+
parentKey[parent.target.fields.find((f) => same(f.name, pair.target))?.name ?? pair.target] = instance.row[master.fields.find((f) => same(f.name, pair.source))?.name ?? pair.source] ?? null;
|
|
193
|
+
}
|
|
194
|
+
master = parent.target;
|
|
195
|
+
masterKey = keyOf(master, parentKey);
|
|
196
|
+
}
|
|
197
|
+
const id = this.id(master, masterKey);
|
|
198
|
+
if (this.locked.has(id)) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
const buffered = this.instances.get(id);
|
|
202
|
+
if (buffered !== undefined && buffered.persisted === undefined) {
|
|
203
|
+
this.locked.add(id);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if (master.entity.kind === "custom") {
|
|
207
|
+
// instances of an API: no database lock, the LUWs of the application are serialized (D43)
|
|
208
|
+
this.locked.add(id);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
const conditions = master.keys.map((k) => sql `${sql.id(master.columns.get(k.name) ?? abapName(k.name))} = ${toSql(k.type, masterKey[k.name] ?? null)}`);
|
|
212
|
+
try {
|
|
213
|
+
await withSavepoint(this.db, () => sql `SELECT 1 FROM ${sql.id(master.table.name)} WHERE ${sql.join(conditions, sql ` AND `)} FOR UPDATE NOWAIT`.execute(this.db));
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (error.code === "55P03") {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
this.locked.add(id);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
/** Global authorization of operations: the result of the authorization master's handler. */
|
|
225
|
+
async globalAuthorization(bo, requested, mode) {
|
|
226
|
+
const denied = new Set();
|
|
227
|
+
const master = this.bo.authorizationMaster(bo);
|
|
228
|
+
const handler = master.behavior.globalAuthorizationHandler;
|
|
229
|
+
if (mode.local || mode.privileged || handler === undefined) {
|
|
230
|
+
return denied;
|
|
231
|
+
}
|
|
232
|
+
const request = this.bridge.data(master, derivedType.globalAuthRequest(master.index));
|
|
233
|
+
const result = this.bridge.data(master, derivedType.globalAuthResult(master.index));
|
|
234
|
+
const flags = master === bo ? requested : [{ kind: "update" }];
|
|
235
|
+
for (const op of flags) {
|
|
236
|
+
setFlag(this.operationFlag(request, op), true);
|
|
237
|
+
}
|
|
238
|
+
const failed = this.bridge.response(master, "failed");
|
|
239
|
+
const reported = this.bridge.response(master, "reported");
|
|
240
|
+
await this.call(handler, { requested: request, result, failed, reported });
|
|
241
|
+
for (const op of requested) {
|
|
242
|
+
const checked = master === bo ? op : { kind: "update" };
|
|
243
|
+
if (isFlagOn(this.operationFlag(result, checked))) {
|
|
244
|
+
denied.add(operationId(op));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return denied;
|
|
248
|
+
}
|
|
249
|
+
/** Instance authorization: denied operations per instance key (of `bo`). */
|
|
250
|
+
async instanceAuthorization(bo, keys, op, mode) {
|
|
251
|
+
const denied = new Set();
|
|
252
|
+
const master = this.bo.authorizationMaster(bo);
|
|
253
|
+
const handler = master.behavior.instanceAuthorizationHandler;
|
|
254
|
+
if (mode.local || mode.privileged || handler === undefined || keys.length === 0) {
|
|
255
|
+
return denied;
|
|
256
|
+
}
|
|
257
|
+
const masterKeys = await Promise.all(keys.map((k) => this.masterKey(bo, master, k)));
|
|
258
|
+
const unique = [...new Map(masterKeys.filter((k) => k !== undefined).map((k) => [keyString(master, k), k])).values()];
|
|
259
|
+
const request = this.bridge.data(master, derivedType.instanceAuthRequest(master.index));
|
|
260
|
+
const checked = master === bo ? op : { kind: "update" };
|
|
261
|
+
setFlag(this.operationFlag(request, checked), true);
|
|
262
|
+
const result = this.bridge.data(master, derivedType.instanceAuthResult(master.index));
|
|
263
|
+
const failed = this.bridge.response(master, "failed");
|
|
264
|
+
const reported = this.bridge.response(master, "reported");
|
|
265
|
+
await this.call(handler, { keys: this.bridge.keysTable(master, unique), requested: request, result, failed, reported });
|
|
266
|
+
const deniedMasters = new Set();
|
|
267
|
+
for (const row of rowsOf(result)) {
|
|
268
|
+
if (isFlagOn(this.operationFlag(row, checked))) {
|
|
269
|
+
deniedMasters.add(keyString(master, this.bridge.readKey(row, master)));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
keys.forEach((key, i) => {
|
|
273
|
+
const masterKey = masterKeys[i];
|
|
274
|
+
if (masterKey !== undefined && deniedMasters.has(keyString(master, masterKey))) {
|
|
275
|
+
denied.add(keyString(bo, key));
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
return denied;
|
|
279
|
+
}
|
|
280
|
+
/** Key of the instance of `master` (an ancestor) that `key` of `bo` belongs to. */
|
|
281
|
+
async masterKey(bo, master, key) {
|
|
282
|
+
let current = bo;
|
|
283
|
+
let currentKey = key;
|
|
284
|
+
for (let depth = 0; depth < 20 && current !== master; depth++) {
|
|
285
|
+
const parent = current.parent;
|
|
286
|
+
const instance = await this.instance(current, currentKey);
|
|
287
|
+
if (parent === undefined || instance === undefined) {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
const parentKey = {};
|
|
291
|
+
for (const pair of parent.on) {
|
|
292
|
+
parentKey[parent.target.fields.find((f) => same(f.name, pair.target))?.name ?? pair.target] = instance.row[current.fields.find((f) => same(f.name, pair.source))?.name ?? pair.source] ?? null;
|
|
293
|
+
}
|
|
294
|
+
current = parent.target;
|
|
295
|
+
currentKey = keyOf(current, parentKey);
|
|
296
|
+
}
|
|
297
|
+
return current === master ? currentKey : undefined;
|
|
298
|
+
}
|
|
299
|
+
/** Instance feature control: operations disabled per instance, and read-only fields. */
|
|
300
|
+
async instanceFeatures(bo, keys, requested, mode) {
|
|
301
|
+
const disabled = new Set();
|
|
302
|
+
const readonly = new Map();
|
|
303
|
+
const handler = bo.behavior.instanceFeaturesHandler;
|
|
304
|
+
if (mode.local || handler === undefined || keys.length === 0) {
|
|
305
|
+
return { disabled, readonly };
|
|
306
|
+
}
|
|
307
|
+
const request = this.bridge.data(bo, derivedType.instanceFeaturesRequest(bo.index));
|
|
308
|
+
for (const op of requested.operations) {
|
|
309
|
+
setFlag(this.operationFlag(request, op), true);
|
|
310
|
+
}
|
|
311
|
+
const fieldRequest = component(request, "%field");
|
|
312
|
+
for (const field of requested.fields) {
|
|
313
|
+
setFlag(fieldRequest === undefined ? undefined : component(fieldRequest, field), true);
|
|
314
|
+
}
|
|
315
|
+
const result = this.bridge.data(bo, derivedType.instanceFeaturesResult(bo.index));
|
|
316
|
+
const failed = this.bridge.response(bo, "failed");
|
|
317
|
+
const reported = this.bridge.response(bo, "reported");
|
|
318
|
+
await this.call(handler, { keys: this.bridge.keysTable(bo, keys), requested: request, result, failed, reported });
|
|
319
|
+
for (const row of rowsOf(result)) {
|
|
320
|
+
const key = keyString(bo, this.bridge.readKey(row, bo));
|
|
321
|
+
for (const op of requested.operations) {
|
|
322
|
+
if (isFlagOn(this.operationFlag(row, op))) {
|
|
323
|
+
disabled.add(`${key}|${operationId(op)}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const fields = component(row, "%field");
|
|
327
|
+
for (const field of requested.fields) {
|
|
328
|
+
const value = fields === undefined ? undefined : component(fields, field);
|
|
329
|
+
const code = value === undefined ? "00" : String(value.get());
|
|
330
|
+
// fc-f-read_only '01' and mandatory_and_read_only '03'
|
|
331
|
+
if (code === "01" || code === "03") {
|
|
332
|
+
const set = readonly.get(key) ?? new Set();
|
|
333
|
+
set.add(field);
|
|
334
|
+
readonly.set(key, set);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return { disabled, readonly };
|
|
339
|
+
}
|
|
340
|
+
/** Global feature control: disabled operations. */
|
|
341
|
+
async globalFeatures(bo, requested, mode) {
|
|
342
|
+
const disabled = new Set();
|
|
343
|
+
const handler = bo.behavior.globalFeaturesHandler;
|
|
344
|
+
if (mode.local || handler === undefined) {
|
|
345
|
+
return disabled;
|
|
346
|
+
}
|
|
347
|
+
const request = this.bridge.data(bo, derivedType.globalFeaturesRequest(bo.index));
|
|
348
|
+
for (const op of requested) {
|
|
349
|
+
setFlag(this.operationFlag(request, op), true);
|
|
350
|
+
}
|
|
351
|
+
const result = this.bridge.data(bo, derivedType.globalFeaturesResult(bo.index));
|
|
352
|
+
const failed = this.bridge.response(bo, "failed");
|
|
353
|
+
const reported = this.bridge.response(bo, "reported");
|
|
354
|
+
await this.call(handler, { requested: request, result, failed, reported });
|
|
355
|
+
for (const op of requested) {
|
|
356
|
+
if (isFlagOn(this.operationFlag(result, op))) {
|
|
357
|
+
disabled.add(operationId(op));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return disabled;
|
|
361
|
+
}
|
|
362
|
+
operationFlag(structure, op) {
|
|
363
|
+
if (op.kind === "action") {
|
|
364
|
+
const action = component(structure, "%action");
|
|
365
|
+
return action === undefined ? undefined : component(action, op.name);
|
|
366
|
+
}
|
|
367
|
+
if (op.kind === "association") {
|
|
368
|
+
const assoc = component(structure, "%assoc");
|
|
369
|
+
return assoc === undefined ? undefined : component(assoc, op.name);
|
|
370
|
+
}
|
|
371
|
+
return component(structure, `%${op.kind}`);
|
|
372
|
+
}
|
|
373
|
+
/** Calls a handler with the parameter names of its definition. */
|
|
374
|
+
async call(ref, args) {
|
|
375
|
+
const params = {};
|
|
376
|
+
for (const [role, value] of Object.entries(args)) {
|
|
377
|
+
if (value !== undefined) {
|
|
378
|
+
params[ref.params[role] ?? role] = value;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
await this.bridge.call(ref, params);
|
|
382
|
+
}
|
|
383
|
+
async precheck(bo, operation, entities, mode) {
|
|
384
|
+
const handler = bo.behavior.precheckHandlers[operation];
|
|
385
|
+
if (handler === undefined || mode.local) {
|
|
386
|
+
return new Responses();
|
|
387
|
+
}
|
|
388
|
+
const failed = this.bridge.response(bo, "failed");
|
|
389
|
+
const reported = this.bridge.response(bo, "reported");
|
|
390
|
+
await this.call(handler, { entities, failed, reported });
|
|
391
|
+
return this.bridge.readResponses(bo, failed, reported);
|
|
392
|
+
}
|
|
393
|
+
// -------------------------------------------------------------------------------------------
|
|
394
|
+
// operations
|
|
395
|
+
setAdminFields(bo, row, create, changed) {
|
|
396
|
+
const now = this.services.now();
|
|
397
|
+
const set = (field, value) => {
|
|
398
|
+
if (field !== undefined) {
|
|
399
|
+
row[field] = value;
|
|
400
|
+
changed.add(field);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
if (create) {
|
|
404
|
+
set(bo.admin.createdBy, this.user.abapName);
|
|
405
|
+
set(bo.admin.createdAt, now);
|
|
406
|
+
}
|
|
407
|
+
set(bo.admin.lastChangedBy, this.user.abapName);
|
|
408
|
+
set(bo.admin.lastChangedAt, now);
|
|
409
|
+
set(bo.admin.localInstanceLastChangedAt, now);
|
|
410
|
+
}
|
|
411
|
+
/** Fields a consumer may not write: read-only, managed numbering, admin fields (outside local mode). */
|
|
412
|
+
writable(bo, field, mode, create) {
|
|
413
|
+
if (mode.local) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
const characteristics = this.bo.characteristics(bo, field);
|
|
417
|
+
if (characteristics.includes("readonly") || characteristics.includes("numberingManaged")) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
if (!create && characteristics.includes("readonlyUpdate")) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
if (!create && bo.keys.some((k) => k.name === field)) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
async create(bo, requests, mode, parent) {
|
|
429
|
+
const responses = new Responses();
|
|
430
|
+
const operation = { kind: "create" };
|
|
431
|
+
if (parent === undefined && !mode.local && mode.external && bo.behavior.operations.find((o) => o.kind === "create")?.internal === true) {
|
|
432
|
+
throw new RapRuntimeError(`create of ${bo.behavior.alias} is internal`);
|
|
433
|
+
}
|
|
434
|
+
if (parent === undefined) {
|
|
435
|
+
const denied = await this.globalAuthorization(bo, [{ kind: "create" }], mode);
|
|
436
|
+
const disabled = bo.behavior.operations.some((o) => o.kind === "create" && o.features === "global") ? await this.globalFeatures(bo, [{ kind: "create" }], mode) : new Set();
|
|
437
|
+
if (denied.size > 0 || disabled.size > 0) {
|
|
438
|
+
for (const request of requests) {
|
|
439
|
+
this.failure(responses, bo, { cause: denied.size > 0 ? "unauthorized" : "disabled", ...(request.cid === undefined ? {} : { cid: request.cid }) }, operation);
|
|
440
|
+
}
|
|
441
|
+
return responses;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// early numbering by a handler: of the creation, or of the creation by association (parent)
|
|
445
|
+
let numbered = parent?.numbered;
|
|
446
|
+
if (parent === undefined && bo.behavior.earlyNumberingHandler !== undefined) {
|
|
447
|
+
numbered = await this.number(bo, requests, responses);
|
|
448
|
+
}
|
|
449
|
+
if (parent === undefined && bo.behavior.operations.some((o) => o.kind === "create" && o.precheck)) {
|
|
450
|
+
const entities = this.createTable(bo, requests);
|
|
451
|
+
responses.add(await this.precheck(bo, "create", entities, mode));
|
|
452
|
+
}
|
|
453
|
+
const prechecked = new Set(responses.failed.filter((f) => f.cid !== undefined).map((f) => f.cid));
|
|
454
|
+
for (const request of requests) {
|
|
455
|
+
if (request.cid !== undefined && prechecked.has(request.cid)) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const row = {};
|
|
459
|
+
const changed = new Set();
|
|
460
|
+
for (const field of bo.fields) {
|
|
461
|
+
row[field.name] = initialValue(field.type);
|
|
462
|
+
}
|
|
463
|
+
for (const field of request.fields) {
|
|
464
|
+
const model = this.bo.field(bo, field);
|
|
465
|
+
if (model !== undefined && this.writable(bo, model.name, mode, true)) {
|
|
466
|
+
row[model.name] = request.values[model.name] ?? initialValue(model.type);
|
|
467
|
+
changed.add(model.name);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (parent !== undefined) {
|
|
471
|
+
// the foreign key of the child comes from the parent (ON condition of the composition)
|
|
472
|
+
for (const pair of parent.association.on) {
|
|
473
|
+
const field = bo.fields.find((f) => same(f.name, pair.target))?.name ?? pair.target;
|
|
474
|
+
row[field] = parent.key[pair.source] ?? null;
|
|
475
|
+
changed.add(field);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const early = request.cid === undefined ? undefined : numbered?.get(request.cid);
|
|
479
|
+
if (early !== undefined) {
|
|
480
|
+
Object.assign(row, early);
|
|
481
|
+
}
|
|
482
|
+
if (mode.keepKeys === true) {
|
|
483
|
+
for (const key of bo.keys) {
|
|
484
|
+
if (request.values[key.name] !== undefined && request.values[key.name] !== null) {
|
|
485
|
+
row[key.name] = request.values[key.name] ?? null;
|
|
486
|
+
changed.add(key.name);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
for (const field of bo.fields) {
|
|
491
|
+
const characteristics = this.bo.characteristics(bo, field.name);
|
|
492
|
+
if (characteristics.includes("numberingManaged") && (isInitial(field.type, row[field.name]) || (!mode.local && mode.keepKeys !== true))) {
|
|
493
|
+
row[field.name] = randomUUID();
|
|
494
|
+
changed.add(field.name);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (!mode.local) {
|
|
498
|
+
const missing = bo.fields.find((f) => this.bo.characteristics(bo, f.name).includes("mandatoryCreate") && isInitial(f.type, row[f.name]));
|
|
499
|
+
if (missing !== undefined) {
|
|
500
|
+
this.failure(responses, bo, { cause: "unspecific", ...(request.cid === undefined ? {} : { cid: request.cid }) }, operation);
|
|
501
|
+
responses.reported.push({
|
|
502
|
+
entity: bo.entity.name,
|
|
503
|
+
...(request.cid === undefined ? {} : { cid: request.cid }),
|
|
504
|
+
message: { severity: "E", text: `${missing.name} is required` },
|
|
505
|
+
elements: [missing.name],
|
|
506
|
+
global: false,
|
|
507
|
+
});
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const key = keyOf(bo, row);
|
|
512
|
+
if ((await this.instance(bo, key)) !== undefined) {
|
|
513
|
+
this.failure(responses, bo, { cause: "conflict", key, ...(request.cid === undefined ? {} : { cid: request.cid }) }, operation);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
this.setAdminFields(bo, row, true, changed);
|
|
517
|
+
const id = this.id(bo, key);
|
|
518
|
+
const previous = this.instances.get(id);
|
|
519
|
+
const instance = { bo, row, deleted: false, changed };
|
|
520
|
+
if (previous?.persisted !== undefined) {
|
|
521
|
+
// re-created after a deletion in the same LUW: an update of the stored row
|
|
522
|
+
instance.persisted = previous.persisted;
|
|
523
|
+
}
|
|
524
|
+
if (request.cid !== undefined && request.cid !== "") {
|
|
525
|
+
instance.cid = request.cid;
|
|
526
|
+
this.cids.set(request.cid, { bo, key });
|
|
527
|
+
responses.mapped.push({ entity: bo.entity.name, cid: request.cid, key });
|
|
528
|
+
}
|
|
529
|
+
this.instances.set(id, instance);
|
|
530
|
+
this.record(bo, key, "create", new Set(changed));
|
|
531
|
+
}
|
|
532
|
+
return responses;
|
|
533
|
+
}
|
|
534
|
+
createTable(bo, requests) {
|
|
535
|
+
const table = this.bridge.data(bo, derivedType.create(bo.index));
|
|
536
|
+
for (const request of requests) {
|
|
537
|
+
appendRow(table, (row) => {
|
|
538
|
+
component(row, "%cid")?.set(request.cid ?? "");
|
|
539
|
+
this.bridge.fillRow(row, bo, request.values, request.fields);
|
|
540
|
+
this.bridge.fillControl(row, bo, request.fields);
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return table;
|
|
544
|
+
}
|
|
545
|
+
/** Early numbering by the FOR NUMBERING handler: %cid → key. */
|
|
546
|
+
async number(bo, requests, responses) {
|
|
547
|
+
const handler = bo.behavior.earlyNumberingHandler;
|
|
548
|
+
const entities = this.createTable(bo, requests);
|
|
549
|
+
const mapped = this.bridge.response(bo, "mapped");
|
|
550
|
+
const failed = this.bridge.response(bo, "failed");
|
|
551
|
+
const reported = this.bridge.response(bo, "reported");
|
|
552
|
+
await this.call(handler, { entities, mapped, failed, reported });
|
|
553
|
+
const result = await this.bridge.readResponses(bo, failed, reported, mapped);
|
|
554
|
+
responses.failed.push(...result.failed);
|
|
555
|
+
responses.reported.push(...result.reported);
|
|
556
|
+
return new Map(result.mapped.filter((m) => m.entity === bo.entity.name).map((m) => [m.cid, m.key]));
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Early numbering of a creation by association (`FOR NUMBERING … FOR CREATE Travel\_Booking`):
|
|
560
|
+
* the handler gets the parent instances with their %target, and maps the %cid of the children.
|
|
561
|
+
*/
|
|
562
|
+
async numberByAssociation(bo, association, parents, responses) {
|
|
563
|
+
const handler = association.behavior?.earlyNumberingHandler;
|
|
564
|
+
const entities = this.bridge.data(bo, derivedType.assocCreate(bo.index, association.index));
|
|
565
|
+
for (const { key, targets } of parents) {
|
|
566
|
+
appendRow(entities, (row) => {
|
|
567
|
+
this.bridge.fillKey(row, bo, key);
|
|
568
|
+
const target = component(row, "%target");
|
|
569
|
+
for (const t of targets) {
|
|
570
|
+
appendRow(target, (r) => {
|
|
571
|
+
component(r, "%cid")?.set(t.cid ?? "");
|
|
572
|
+
this.bridge.fillRow(r, association.target, t.values, t.fields);
|
|
573
|
+
this.bridge.fillControl(r, association.target, t.fields);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
const mapped = this.bridge.response(bo, "mapped");
|
|
579
|
+
const failed = this.bridge.response(bo, "failed");
|
|
580
|
+
const reported = this.bridge.response(bo, "reported");
|
|
581
|
+
await this.call(handler, { entities, mapped, failed, reported });
|
|
582
|
+
const result = await this.bridge.readResponses(bo, failed, reported, mapped);
|
|
583
|
+
responses.failed.push(...result.failed);
|
|
584
|
+
responses.reported.push(...result.reported);
|
|
585
|
+
return new Map(result.mapped.filter((m) => m.entity === association.target.entity.name).map((m) => [m.cid, m.key]));
|
|
586
|
+
}
|
|
587
|
+
async update(bo, requests, mode) {
|
|
588
|
+
const responses = new Responses();
|
|
589
|
+
const operation = { kind: "update" };
|
|
590
|
+
if (!mode.local && mode.external && bo.behavior.operations.find((o) => o.kind === "update")?.internal === true) {
|
|
591
|
+
throw new RapRuntimeError(`update of ${bo.behavior.alias} is internal`);
|
|
592
|
+
}
|
|
593
|
+
const targets = [];
|
|
594
|
+
for (const request of requests) {
|
|
595
|
+
const key = this.resolveKey(bo, request.key, request.cidRef);
|
|
596
|
+
const instance = key === undefined ? undefined : await this.instance(bo, key);
|
|
597
|
+
if (key === undefined || instance === undefined) {
|
|
598
|
+
this.failure(responses, bo, { cause: "not_found", ...(key === undefined ? {} : { key }), ...(request.cidRef === undefined ? {} : { cid: request.cidRef }) }, operation);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
targets.push({ request, key, instance });
|
|
602
|
+
}
|
|
603
|
+
const keys = targets.map((t) => t.key);
|
|
604
|
+
const denied = await this.globalAuthorization(bo, [operation], mode);
|
|
605
|
+
const deniedInstances = await this.instanceAuthorization(bo, keys, operation, mode);
|
|
606
|
+
const dynamicFields = bo.fields.filter((f) => this.bo.characteristics(bo, f.name).includes("featuresInstance")).map((f) => f.name);
|
|
607
|
+
const updateDynamic = bo.behavior.operations.some((o) => o.kind === "update" && o.features === "instance");
|
|
608
|
+
const features = await this.instanceFeatures(bo, keys, { operations: updateDynamic ? [operation] : [], fields: dynamicFields }, mode);
|
|
609
|
+
const globalDisabled = bo.behavior.operations.some((o) => o.kind === "update" && o.features === "global") ? await this.globalFeatures(bo, [operation], mode) : new Set();
|
|
610
|
+
if (bo.behavior.operations.some((o) => o.kind === "update" && o.precheck) && !mode.local) {
|
|
611
|
+
const table = this.bridge.data(bo, derivedType.update(bo.index));
|
|
612
|
+
for (const { request, key } of targets) {
|
|
613
|
+
appendRow(table, (row) => {
|
|
614
|
+
this.bridge.fillRow(row, bo, { ...request.values, ...key }, [...request.fields, ...bo.keys.map((k) => k.name)]);
|
|
615
|
+
this.bridge.fillControl(row, bo, request.fields);
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
responses.add(await this.precheck(bo, "update", table, mode));
|
|
619
|
+
}
|
|
620
|
+
const prechecked = new Set(responses.failed.map((f) => (f.key === undefined ? "" : keyString(bo, f.key))));
|
|
621
|
+
for (const { request, key, instance } of targets) {
|
|
622
|
+
const k = keyString(bo, key);
|
|
623
|
+
if (prechecked.has(k)) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
const cause = denied.size > 0 || deniedInstances.has(k) ? "unauthorized" : globalDisabled.size > 0 || features.disabled.has(`${k}|update`) ? "disabled" : undefined;
|
|
627
|
+
if (cause !== undefined) {
|
|
628
|
+
this.failure(responses, bo, { cause, key }, operation);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
const readonly = features.readonly.get(k);
|
|
632
|
+
const blocked = [...request.fields].find((f) => readonly?.has(f) === true);
|
|
633
|
+
if (blocked !== undefined) {
|
|
634
|
+
this.failure(responses, bo, { cause: "readonly", key }, operation);
|
|
635
|
+
responses.reported.push({ entity: bo.entity.name, key, message: { severity: "E", text: `${blocked} is read-only` }, elements: [blocked], global: false });
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (!(await this.lock(bo, key))) {
|
|
639
|
+
this.failure(responses, bo, { cause: "locked", key }, operation);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const changed = new Set();
|
|
643
|
+
for (const field of request.fields) {
|
|
644
|
+
const model = this.bo.field(bo, field);
|
|
645
|
+
if (model === undefined || model.key || !this.writable(bo, model.name, mode, false)) {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
instance.row[model.name] = request.values[model.name] ?? initialValue(model.type);
|
|
649
|
+
changed.add(model.name);
|
|
650
|
+
}
|
|
651
|
+
this.setAdminFields(bo, instance.row, false, changed);
|
|
652
|
+
for (const f of changed) {
|
|
653
|
+
instance.changed.add(f);
|
|
654
|
+
}
|
|
655
|
+
this.record(bo, key, "update", changed);
|
|
656
|
+
}
|
|
657
|
+
return responses;
|
|
658
|
+
}
|
|
659
|
+
async delete(bo, requests, mode) {
|
|
660
|
+
const responses = new Responses();
|
|
661
|
+
const operation = { kind: "delete" };
|
|
662
|
+
if (!mode.local && mode.external && bo.behavior.operations.find((o) => o.kind === "delete")?.internal === true) {
|
|
663
|
+
throw new RapRuntimeError(`delete of ${bo.behavior.alias} is internal`);
|
|
664
|
+
}
|
|
665
|
+
const targets = [];
|
|
666
|
+
for (const request of requests) {
|
|
667
|
+
const key = this.resolveKey(bo, request.key, request.cidRef);
|
|
668
|
+
const instance = key === undefined ? undefined : await this.instance(bo, key);
|
|
669
|
+
if (key === undefined || instance === undefined) {
|
|
670
|
+
this.failure(responses, bo, { cause: "not_found", ...(key === undefined ? {} : { key }) }, operation);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
targets.push({ key, instance });
|
|
674
|
+
}
|
|
675
|
+
const keys = targets.map((t) => t.key);
|
|
676
|
+
const denied = await this.globalAuthorization(bo, [operation], mode);
|
|
677
|
+
const deniedInstances = await this.instanceAuthorization(bo, keys, operation, mode);
|
|
678
|
+
const deleteDynamic = bo.behavior.operations.some((o) => o.kind === "delete" && o.features === "instance");
|
|
679
|
+
const features = deleteDynamic ? await this.instanceFeatures(bo, keys, { operations: [operation], fields: [] }, mode) : { disabled: new Set(), readonly: new Map() };
|
|
680
|
+
const globalDisabled = bo.behavior.operations.some((o) => o.kind === "delete" && o.features === "global") ? await this.globalFeatures(bo, [operation], mode) : new Set();
|
|
681
|
+
if (bo.behavior.operations.some((o) => o.kind === "delete" && o.precheck) && !mode.local) {
|
|
682
|
+
responses.add(await this.precheck(bo, "delete", this.bridge.keysTable(bo, keys, derivedType.delete(bo.index)), mode));
|
|
683
|
+
}
|
|
684
|
+
const prechecked = new Set(responses.failed.map((f) => (f.key === undefined ? "" : keyString(bo, f.key))));
|
|
685
|
+
for (const { key, instance } of targets) {
|
|
686
|
+
const k = keyString(bo, key);
|
|
687
|
+
if (prechecked.has(k)) {
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
const cause = denied.size > 0 || deniedInstances.has(k) ? "unauthorized" : globalDisabled.size > 0 || features.disabled.has(`${k}|delete`) ? "disabled" : undefined;
|
|
691
|
+
if (cause !== undefined) {
|
|
692
|
+
this.failure(responses, bo, { cause, key }, operation);
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (!(await this.lock(bo, key))) {
|
|
696
|
+
this.failure(responses, bo, { cause: "locked", key }, operation);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
await this.cascade(bo, instance);
|
|
700
|
+
}
|
|
701
|
+
return responses;
|
|
702
|
+
}
|
|
703
|
+
/** Deletes an instance and its composition children. */
|
|
704
|
+
async cascade(bo, instance) {
|
|
705
|
+
for (const composition of bo.compositions) {
|
|
706
|
+
for (const child of await this.associated(bo, composition, instance.row)) {
|
|
707
|
+
const childInstance = await this.instance(composition.target, keyOf(composition.target, child));
|
|
708
|
+
if (childInstance !== undefined) {
|
|
709
|
+
await this.cascade(composition.target, childInstance);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
instance.deleted = true;
|
|
714
|
+
this.record(bo, keyOf(bo, instance.row), "delete", new Set());
|
|
715
|
+
}
|
|
716
|
+
async createByAssociation(bo, association, requests, mode) {
|
|
717
|
+
const responses = new Responses();
|
|
718
|
+
const operation = { kind: "association", name: association.name };
|
|
719
|
+
if (!mode.local && mode.external && (association.behavior?.createInternal === true || association.behavior?.create !== true)) {
|
|
720
|
+
throw new RapRuntimeError(`create by association ${bo.behavior.alias}\\${association.name} is not allowed`);
|
|
721
|
+
}
|
|
722
|
+
const targets = [];
|
|
723
|
+
for (const request of requests) {
|
|
724
|
+
const key = this.resolveKey(bo, request.key, request.cidRef);
|
|
725
|
+
const instance = key === undefined ? undefined : await this.instance(bo, key);
|
|
726
|
+
if (key === undefined || instance === undefined) {
|
|
727
|
+
this.failure(responses, bo, { cause: "not_found", ...(key === undefined ? {} : { key }) }, operation);
|
|
728
|
+
for (const target of request.targets) {
|
|
729
|
+
this.failure(responses, association.target, { cause: "dependency", ...(target.cid === undefined ? {} : { cid: target.cid }) }, { kind: "create" });
|
|
730
|
+
}
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
targets.push({ request, key, instance });
|
|
734
|
+
}
|
|
735
|
+
const keys = targets.map((t) => t.key);
|
|
736
|
+
const denied = await this.globalAuthorization(bo, [operation], mode);
|
|
737
|
+
const deniedInstances = await this.instanceAuthorization(bo, keys, operation, mode);
|
|
738
|
+
const dynamic = association.behavior?.createFeatures === "instance";
|
|
739
|
+
const features = dynamic ? await this.instanceFeatures(bo, keys, { operations: [operation], fields: [] }, mode) : { disabled: new Set(), readonly: new Map() };
|
|
740
|
+
if (association.behavior?.createPrecheck === true && !mode.local) {
|
|
741
|
+
const table = this.bridge.data(bo, derivedType.assocCreate(bo.index, association.index));
|
|
742
|
+
for (const { request, key } of targets) {
|
|
743
|
+
appendRow(table, (row) => {
|
|
744
|
+
this.bridge.fillKey(row, bo, key);
|
|
745
|
+
const target = component(row, "%target");
|
|
746
|
+
for (const t of request.targets) {
|
|
747
|
+
appendRow(target, (r) => {
|
|
748
|
+
component(r, "%cid")?.set(t.cid ?? "");
|
|
749
|
+
this.bridge.fillRow(r, association.target, t.values, t.fields);
|
|
750
|
+
this.bridge.fillControl(r, association.target, t.fields);
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
responses.add(await this.precheck(bo, `create:${association.name.toLowerCase()}`, table, mode));
|
|
756
|
+
}
|
|
757
|
+
const prechecked = new Set(responses.failed.map((f) => (f.key === undefined ? "" : keyString(bo, f.key))));
|
|
758
|
+
const allowed = [];
|
|
759
|
+
for (const target of targets) {
|
|
760
|
+
const k = keyString(bo, target.key);
|
|
761
|
+
if (prechecked.has(k)) {
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const cause = denied.size > 0 || deniedInstances.has(k) ? "unauthorized" : features.disabled.has(`${k}|${operationId(operation)}`) ? "disabled" : undefined;
|
|
765
|
+
const locked = cause === undefined ? await this.lock(bo, target.key) : true;
|
|
766
|
+
if (cause !== undefined || !locked) {
|
|
767
|
+
this.failure(responses, bo, { cause: cause ?? "locked", key: target.key }, operation);
|
|
768
|
+
for (const child of target.request.targets) {
|
|
769
|
+
this.failure(responses, association.target, { cause: "dependency", ...(child.cid === undefined ? {} : { cid: child.cid }) }, { kind: "create" });
|
|
770
|
+
}
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
allowed.push(target);
|
|
774
|
+
}
|
|
775
|
+
// early numbering of the children: one call of the handler for all the parents
|
|
776
|
+
const numbered = association.behavior?.earlyNumberingHandler === undefined || allowed.length === 0
|
|
777
|
+
? undefined
|
|
778
|
+
: await this.numberByAssociation(bo, association, allowed.map((t) => ({ key: t.key, targets: t.request.targets })), responses);
|
|
779
|
+
const refused = new Set(responses.failed.filter((f) => f.entity === association.target.entity.name && f.cid !== undefined).map((f) => f.cid));
|
|
780
|
+
for (const { request, instance } of allowed) {
|
|
781
|
+
const parentKey = {};
|
|
782
|
+
for (const pair of association.on) {
|
|
783
|
+
parentKey[pair.source] = instance.row[bo.fields.find((f) => same(f.name, pair.source))?.name ?? pair.source] ?? null;
|
|
784
|
+
}
|
|
785
|
+
const children = request.targets.filter((t) => t.cid === undefined || !refused.has(t.cid));
|
|
786
|
+
responses.add(await this.create(association.target, children, mode, { association, key: parentKey, ...(numbered === undefined ? {} : { numbered }) }));
|
|
787
|
+
}
|
|
788
|
+
return responses;
|
|
789
|
+
}
|
|
790
|
+
// -------------------------------------------------------------------------------------------
|
|
791
|
+
// reads
|
|
792
|
+
async read(bo, keys) {
|
|
793
|
+
const responses = new Responses();
|
|
794
|
+
const rows = [];
|
|
795
|
+
for (const { key: given, cidRef } of keys) {
|
|
796
|
+
const key = this.resolveKey(bo, given, cidRef);
|
|
797
|
+
const instance = key === undefined ? undefined : await this.instance(bo, key);
|
|
798
|
+
if (instance === undefined) {
|
|
799
|
+
this.failure(responses, bo, { cause: "not_found", ...(key === undefined ? {} : { key }) });
|
|
800
|
+
rows.push(undefined);
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
rows.push({ ...instance.row });
|
|
804
|
+
}
|
|
805
|
+
return { rows, responses };
|
|
806
|
+
}
|
|
807
|
+
async readByAssociation(bo, association, keys) {
|
|
808
|
+
const responses = new Responses();
|
|
809
|
+
const results = [];
|
|
810
|
+
for (const given of keys) {
|
|
811
|
+
const key = keyOf(bo, given);
|
|
812
|
+
const instance = await this.instance(bo, key);
|
|
813
|
+
if (instance === undefined) {
|
|
814
|
+
this.failure(responses, bo, { cause: "not_found", key }, { kind: "association", name: association.name });
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
for (const target of await this.associated(bo, association, instance.row)) {
|
|
818
|
+
results.push({ source: key, target: { ...target } });
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return { results, responses };
|
|
822
|
+
}
|
|
823
|
+
// -------------------------------------------------------------------------------------------
|
|
824
|
+
// actions and functions
|
|
825
|
+
async execute(bo, action, requests, mode) {
|
|
826
|
+
const responses = new Responses();
|
|
827
|
+
const results = [];
|
|
828
|
+
const operation = { kind: "action", name: action.name };
|
|
829
|
+
if (!mode.local && mode.external && action.internal) {
|
|
830
|
+
throw new RapRuntimeError(`${action.kind} ${bo.behavior.alias}~${action.name} is internal`);
|
|
831
|
+
}
|
|
832
|
+
if (action.handler === undefined) {
|
|
833
|
+
throw new RapRuntimeError(`no handler for ${bo.behavior.alias}~${action.name}`);
|
|
834
|
+
}
|
|
835
|
+
const actionIndex = bo.behavior.actions.indexOf(action) + 1;
|
|
836
|
+
const accepted = [];
|
|
837
|
+
for (const request of requests) {
|
|
838
|
+
if (action.static) {
|
|
839
|
+
accepted.push({ request });
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
const key = this.resolveKey(bo, request.key, request.cidRef);
|
|
843
|
+
const instance = key === undefined ? undefined : await this.instance(bo, key);
|
|
844
|
+
if (key === undefined || instance === undefined) {
|
|
845
|
+
this.failure(responses, bo, { cause: "not_found", ...(key === undefined ? {} : { key }) }, operation);
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
accepted.push({ request, key });
|
|
849
|
+
}
|
|
850
|
+
const keys = accepted.map((a) => a.key).filter((k) => k !== undefined);
|
|
851
|
+
const denied = await this.globalAuthorization(bo, [operation], mode);
|
|
852
|
+
const deniedInstances = action.static ? new Set() : await this.instanceAuthorization(bo, keys, operation, mode);
|
|
853
|
+
const features = action.features === "instance" ? await this.instanceFeatures(bo, keys, { operations: [operation], fields: [] }, mode) : { disabled: new Set(), readonly: new Map() };
|
|
854
|
+
const globalDisabled = action.features === "global" ? await this.globalFeatures(bo, [operation], mode) : new Set();
|
|
855
|
+
const run = [];
|
|
856
|
+
for (const item of accepted) {
|
|
857
|
+
const k = item.key === undefined ? "" : keyString(bo, item.key);
|
|
858
|
+
const cause = denied.size > 0 || deniedInstances.has(k) ? "unauthorized" : globalDisabled.size > 0 || features.disabled.has(`${k}|${operationId(operation)}`) ? "disabled" : undefined;
|
|
859
|
+
if (cause !== undefined) {
|
|
860
|
+
this.failure(responses, bo, { cause, ...(item.key === undefined ? {} : { key: item.key }), ...(item.request.cid === undefined ? {} : { cid: item.request.cid }) }, operation);
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (action.kind === "action" && item.key !== undefined && !(await this.lock(bo, item.key))) {
|
|
864
|
+
this.failure(responses, bo, { cause: "locked", key: item.key }, operation);
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
run.push(item);
|
|
868
|
+
}
|
|
869
|
+
if (run.length === 0) {
|
|
870
|
+
return { results, responses };
|
|
871
|
+
}
|
|
872
|
+
const parameterEntity = action.parameter === undefined || action.parameter === "$self" ? undefined : this.bo.model.entities.find((e) => same(e.name, action.parameter ?? ""));
|
|
873
|
+
const importTable = this.bridge.data(bo, derivedType.actionImport(bo.index, actionIndex));
|
|
874
|
+
for (const item of run) {
|
|
875
|
+
appendRow(importTable, (row) => {
|
|
876
|
+
if (item.key !== undefined) {
|
|
877
|
+
this.bridge.fillKey(row, bo, item.key);
|
|
878
|
+
}
|
|
879
|
+
component(row, action.factory || action.static ? "%cid" : "%cid_ref")?.set((action.factory || action.static ? item.request.cid : item.request.cidRef) ?? "");
|
|
880
|
+
if (parameterEntity !== undefined) {
|
|
881
|
+
this.bridge.fillParameter(row, parameterEntity.fields, item.request.param);
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
if (action.precheck && !mode.local) {
|
|
886
|
+
responses.add(await this.precheck(bo, `action:${action.name.toLowerCase()}`, importTable, mode));
|
|
887
|
+
}
|
|
888
|
+
const result = action.result === undefined ? undefined : this.bridge.data(bo, derivedType.actionResult(bo.index, actionIndex));
|
|
889
|
+
const mapped = this.bridge.response(bo, "mapped");
|
|
890
|
+
const failed = this.bridge.response(bo, "failed");
|
|
891
|
+
const reported = this.bridge.response(bo, "reported");
|
|
892
|
+
await this.call(action.handler, { keys: importTable, ...(result === undefined ? {} : { result }), ...(action.kind === "action" ? { mapped } : {}), failed, reported });
|
|
893
|
+
responses.add(await this.bridge.readResponses(bo, failed, reported, action.kind === "action" ? mapped : undefined));
|
|
894
|
+
for (const m of responses.mapped) {
|
|
895
|
+
const target = this.bo.find(m.entity);
|
|
896
|
+
if (target !== undefined && m.cid !== "") {
|
|
897
|
+
this.cids.set(m.cid, { bo: target, key: m.key });
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const resultEntity = action.result === undefined || action.result.entity === "$self" ? bo : this.bo.find(action.result.entity);
|
|
901
|
+
const abstractResult = action.result !== undefined && resultEntity === undefined ? this.bo.model.entities.find((e) => same(e.name, action.result?.entity ?? "")) : undefined;
|
|
902
|
+
for (const row of rowsOf(result)) {
|
|
903
|
+
const entry = {};
|
|
904
|
+
if (!action.static) {
|
|
905
|
+
entry.key = this.bridge.readKey(row, bo);
|
|
906
|
+
}
|
|
907
|
+
const cid = this.bridge.readText(row, action.static ? "%cid" : "%cid_ref");
|
|
908
|
+
if (cid !== "") {
|
|
909
|
+
if (action.static) {
|
|
910
|
+
entry.cid = cid;
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
entry.cidRef = cid;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const param = component(row, "%param");
|
|
917
|
+
if (param !== undefined && resultEntity !== undefined) {
|
|
918
|
+
entry.value = this.bridge.readRow(param, resultEntity);
|
|
919
|
+
}
|
|
920
|
+
else if (param !== undefined && abstractResult !== undefined) {
|
|
921
|
+
entry.value = this.bridge.readParameter(row, abstractResult.fields);
|
|
922
|
+
}
|
|
923
|
+
results.push(entry);
|
|
924
|
+
}
|
|
925
|
+
return { results, responses };
|
|
926
|
+
}
|
|
927
|
+
// -------------------------------------------------------------------------------------------
|
|
928
|
+
// determinations and validations
|
|
929
|
+
matches(triggers, kind, fields) {
|
|
930
|
+
if (kind === "delete") {
|
|
931
|
+
return triggers.delete;
|
|
932
|
+
}
|
|
933
|
+
if (kind === "create" && triggers.create) {
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
if (kind === "update" && triggers.update) {
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
return triggers.fields.some((f) => [...fields].some((g) => same(f, g)));
|
|
940
|
+
}
|
|
941
|
+
/** Keys of the instances whose triggers were touched since `since`, not by `id` itself. */
|
|
942
|
+
triggered(bo, triggers, since, id) {
|
|
943
|
+
const keys = new Map();
|
|
944
|
+
for (const event of this.events) {
|
|
945
|
+
if (event.seq <= since || event.bo !== bo || event.source === id) {
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (this.matches(triggers, event.kind, event.fields)) {
|
|
949
|
+
keys.set(keyString(bo, event.key), event.key);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return [...keys.values()].filter((key) => {
|
|
953
|
+
const instance = this.instances.get(this.id(bo, key));
|
|
954
|
+
return triggers.delete || (instance !== undefined && !instance.deleted);
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
/** Keys of the instances changed in the LUW in a way that matches the triggers. */
|
|
958
|
+
triggeredInLuw(bo, triggers) {
|
|
959
|
+
const result = [];
|
|
960
|
+
for (const instance of this.instances.values()) {
|
|
961
|
+
if (instance.bo !== bo) {
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
const created = instance.persisted === undefined;
|
|
965
|
+
if (instance.deleted) {
|
|
966
|
+
if (triggers.delete && !created) {
|
|
967
|
+
result.push(keyOf(bo, instance.row));
|
|
968
|
+
}
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
if (instance.changed.size === 0 && !created) {
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
if (this.matches(triggers, created ? "create" : "update", instance.changed)) {
|
|
975
|
+
result.push(keyOf(bo, instance.row));
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return result;
|
|
979
|
+
}
|
|
980
|
+
async runDetermination(bo, determination, keys, late) {
|
|
981
|
+
if (determination.handler === undefined) {
|
|
982
|
+
return new Responses();
|
|
983
|
+
}
|
|
984
|
+
const failed = this.bridge.response(bo, "failed", late);
|
|
985
|
+
const reported = this.bridge.response(bo, "reported", late);
|
|
986
|
+
const previous = this.source;
|
|
987
|
+
this.source = `${bo.entity.name}~${determination.name}`;
|
|
988
|
+
try {
|
|
989
|
+
await this.call(determination.handler, { keys: this.bridge.keysTable(bo, keys), failed, reported });
|
|
990
|
+
}
|
|
991
|
+
finally {
|
|
992
|
+
this.source = previous;
|
|
993
|
+
}
|
|
994
|
+
return this.bridge.readResponses(bo, failed, reported);
|
|
995
|
+
}
|
|
996
|
+
/** Determinations on modify, by rounds (D21). Called at the end of each modify request. */
|
|
997
|
+
async determineOnModify() {
|
|
998
|
+
return this.determine("modify");
|
|
999
|
+
}
|
|
1000
|
+
async determine(on) {
|
|
1001
|
+
const responses = new Responses();
|
|
1002
|
+
const start = this.seq;
|
|
1003
|
+
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
1004
|
+
let ran = false;
|
|
1005
|
+
for (const bo of this.bo.entities) {
|
|
1006
|
+
for (const determination of bo.behavior.determinations.filter((d) => d.on === on)) {
|
|
1007
|
+
const id = `${on}:${bo.entity.name}~${determination.name}`;
|
|
1008
|
+
const source = `${bo.entity.name}~${determination.name}`;
|
|
1009
|
+
let keys;
|
|
1010
|
+
if (on === "save" && !this.seen.has(id)) {
|
|
1011
|
+
keys = this.triggeredInLuw(bo, determination.triggers);
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
keys = this.triggered(bo, determination.triggers, this.seen.get(id) ?? (on === "save" ? start : 0), source);
|
|
1015
|
+
}
|
|
1016
|
+
this.seen.set(id, this.seq);
|
|
1017
|
+
if (keys.length === 0) {
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
ran = true;
|
|
1021
|
+
responses.add(await this.runDetermination(bo, determination, keys, on === "save"));
|
|
1022
|
+
this.seen.set(id, Math.max(this.seen.get(id) ?? 0, this.seq));
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (!ran) {
|
|
1026
|
+
return responses;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
throw new RapRuntimeError(`the determinations on ${on} did not end after ${MAX_ROUNDS} rounds`);
|
|
1030
|
+
}
|
|
1031
|
+
// -------------------------------------------------------------------------------------------
|
|
1032
|
+
// save
|
|
1033
|
+
/** The save sequence: determinations on save, validations; the write when nothing failed. */
|
|
1034
|
+
async save() {
|
|
1035
|
+
if (this.saved) {
|
|
1036
|
+
throw new RapRuntimeError("the transaction is already saved");
|
|
1037
|
+
}
|
|
1038
|
+
const responses = await this.determine("save");
|
|
1039
|
+
if (this.bo.model.mode === "enhanced") {
|
|
1040
|
+
responses.add(this.checkMandatory());
|
|
1041
|
+
}
|
|
1042
|
+
for (const bo of this.bo.entities) {
|
|
1043
|
+
for (const validation of bo.behavior.validations) {
|
|
1044
|
+
const keys = this.triggeredInLuw(bo, validation.triggers);
|
|
1045
|
+
if (keys.length > 0) {
|
|
1046
|
+
responses.add(await this.runDetermination(bo, validation, keys, true));
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if (responses.hasFailed) {
|
|
1051
|
+
return responses;
|
|
1052
|
+
}
|
|
1053
|
+
await this.refreshEtagMasters();
|
|
1054
|
+
await this.write();
|
|
1055
|
+
// additional and unmanaged save: save_modified, whose failed (cl_abap_behavior_saver_failed)
|
|
1056
|
+
// makes the caller roll the LUW back
|
|
1057
|
+
responses.add(await this.saveModified());
|
|
1058
|
+
this.saved = !responses.hasFailed;
|
|
1059
|
+
return responses;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Enhanced mode (D45): `field ( mandatory )` is checked when saving, as a validation on save
|
|
1063
|
+
* would (on SAP, the mandatory characteristic is only a hint for the consumer, D20): each
|
|
1064
|
+
* created or changed instance with an initial mandatory field fails, with a message per field.
|
|
1065
|
+
*/
|
|
1066
|
+
checkMandatory() {
|
|
1067
|
+
const responses = new Responses();
|
|
1068
|
+
for (const instance of this.instances.values()) {
|
|
1069
|
+
const created = instance.persisted === undefined;
|
|
1070
|
+
if (instance.deleted || (!created && instance.changed.size === 0)) {
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
const bo = instance.bo;
|
|
1074
|
+
const missing = bo.fields.filter((field) => {
|
|
1075
|
+
const characteristics = this.bo.characteristics(bo, field.name);
|
|
1076
|
+
return (characteristics.includes("mandatory") || (created && characteristics.includes("mandatoryCreate"))) && isInitial(field.type, instance.row[field.name]);
|
|
1077
|
+
});
|
|
1078
|
+
if (missing.length === 0) {
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
const key = keyOf(bo, instance.row);
|
|
1082
|
+
this.failure(responses, bo, { cause: "unspecific", key });
|
|
1083
|
+
for (const field of missing) {
|
|
1084
|
+
const label = getAnnotation(field.annotations, "EndUserText.label");
|
|
1085
|
+
responses.reported.push({
|
|
1086
|
+
entity: bo.entity.name,
|
|
1087
|
+
key,
|
|
1088
|
+
message: { severity: "E", text: `${typeof label === "string" ? label : field.name} is mandatory` },
|
|
1089
|
+
elements: [field.name],
|
|
1090
|
+
global: false,
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return responses;
|
|
1095
|
+
}
|
|
1096
|
+
/** The changes of the LUW for save_modified: created, updated and deleted instances. */
|
|
1097
|
+
changes(bo) {
|
|
1098
|
+
const result = { created: [], updated: [], deleted: [] };
|
|
1099
|
+
for (const instance of this.instances.values()) {
|
|
1100
|
+
if (instance.bo !== bo) {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
if (instance.deleted) {
|
|
1104
|
+
if (instance.persisted !== undefined) {
|
|
1105
|
+
result.deleted.push(instance);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
else if (instance.persisted === undefined) {
|
|
1109
|
+
result.created.push(instance);
|
|
1110
|
+
}
|
|
1111
|
+
else if (instance.changed.size > 0) {
|
|
1112
|
+
result.updated.push(instance);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return result;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* `INSERT|UPDATE|MODIFY|DELETE dbtab FROM [TABLE] @source [INDICATORS SET STRUCTURE %control]
|
|
1119
|
+
* MAPPING FROM ENTITY` in save_modified: the rows of the entity are written to the table of its
|
|
1120
|
+
* BDEF mapping. sy-subrc 4 when a row is not found (UPDATE, DELETE) or already there (INSERT of
|
|
1121
|
+
* a work area); an INSERT FROM TABLE with a duplicate key is a runtime error, as in ABAP.
|
|
1122
|
+
*/
|
|
1123
|
+
async writeMapped(bo, operation, rows, single, indicators) {
|
|
1124
|
+
if (!this.inSaver) {
|
|
1125
|
+
throw new RapRuntimeError(`${operation} on a database table is only possible in save_modified (with additional or unmanaged save)`);
|
|
1126
|
+
}
|
|
1127
|
+
const mapped = bo.mapped;
|
|
1128
|
+
if (mapped === undefined) {
|
|
1129
|
+
throw new RapRuntimeError(`${bo.entity.name} has no table mapping`);
|
|
1130
|
+
}
|
|
1131
|
+
const table = sql.id(mapped.table.name);
|
|
1132
|
+
const columns = [...mapped.columns.entries()];
|
|
1133
|
+
const isKey = (field) => bo.keys.some((k) => k.name === field);
|
|
1134
|
+
const typeOf = (field) => this.bo.field(bo, field).type;
|
|
1135
|
+
let dbcnt = 0;
|
|
1136
|
+
let missing = false;
|
|
1137
|
+
for (const { values, control } of rows) {
|
|
1138
|
+
const value = (field) => toSql(typeOf(field), values[field] ?? null);
|
|
1139
|
+
const where = sql.join(columns.filter(([f]) => isKey(f)).map(([f, c]) => sql `${sql.id(c)} = ${value(f)}`), sql ` AND `);
|
|
1140
|
+
const names = sql.join(columns.map(([, c]) => sql.id(c)));
|
|
1141
|
+
const list = sql.join(columns.map(([f]) => value(f)));
|
|
1142
|
+
if (operation === "INSERT") {
|
|
1143
|
+
try {
|
|
1144
|
+
await withSavepoint(this.db, () => sql `INSERT INTO ${table} (${names}) VALUES (${list})`.execute(this.db));
|
|
1145
|
+
dbcnt++;
|
|
1146
|
+
}
|
|
1147
|
+
catch (error) {
|
|
1148
|
+
if (error.code !== "23505") {
|
|
1149
|
+
throw error;
|
|
1150
|
+
}
|
|
1151
|
+
if (!single) {
|
|
1152
|
+
throw new RapRuntimeError(`INSERT ${mapped.table.name} FROM TABLE: a row with the same key exists (CX_SY_OPEN_SQL_DB)`);
|
|
1153
|
+
}
|
|
1154
|
+
missing = true;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
else if (operation === "UPDATE") {
|
|
1158
|
+
const sets = columns.filter(([f]) => !isKey(f) && (!indicators || control?.has(f) === true)).map(([f, c]) => sql `${sql.id(c)} = ${value(f)}`);
|
|
1159
|
+
if (sets.length === 0) {
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
const result = await sql `UPDATE ${table} SET ${sql.join(sets)} WHERE ${where}`.execute(this.db);
|
|
1163
|
+
const count = Number(result.numAffectedRows ?? 0n);
|
|
1164
|
+
dbcnt += count;
|
|
1165
|
+
missing ||= count === 0;
|
|
1166
|
+
}
|
|
1167
|
+
else if (operation === "MODIFY") {
|
|
1168
|
+
const keys = sql.join(columns.filter(([f]) => isKey(f)).map(([, c]) => sql.id(c)));
|
|
1169
|
+
const sets = columns.filter(([f]) => !isKey(f)).map(([, c]) => sql `${sql.id(c)} = EXCLUDED.${sql.id(c)}`);
|
|
1170
|
+
await sql `INSERT INTO ${table} (${names}) VALUES (${list}) ON CONFLICT (${keys}) DO ${sets.length === 0 ? sql `NOTHING` : sql `UPDATE SET ${sql.join(sets)}`}`.execute(this.db);
|
|
1171
|
+
dbcnt++;
|
|
1172
|
+
}
|
|
1173
|
+
else if (operation === "DELETE") {
|
|
1174
|
+
const result = await sql `DELETE FROM ${table} WHERE ${where}`.execute(this.db);
|
|
1175
|
+
const count = Number(result.numAffectedRows ?? 0n);
|
|
1176
|
+
dbcnt += count;
|
|
1177
|
+
missing ||= count === 0;
|
|
1178
|
+
}
|
|
1179
|
+
else {
|
|
1180
|
+
throw new RapRuntimeError(`unknown ABAP SQL operation ${operation}`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return { subrc: missing ? 4 : 0, dbcnt };
|
|
1184
|
+
}
|
|
1185
|
+
/** save_modified (and cleanup) of the BDEFs with additional or unmanaged save. */
|
|
1186
|
+
async saveModified() {
|
|
1187
|
+
const responses = new Responses();
|
|
1188
|
+
for (const bdef of new Set(this.bo.entities.map((e) => e.bdef))) {
|
|
1189
|
+
const saver = bdef.saver;
|
|
1190
|
+
const entities = this.bo.entities.filter((e) => e.bdef === bdef && e.saving !== "managed");
|
|
1191
|
+
const first = entities[0];
|
|
1192
|
+
if (saver?.saveModified === undefined || first === undefined) {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const create = this.bridge.data(first, derivedType.requestChange);
|
|
1196
|
+
const update = this.bridge.data(first, derivedType.requestChange);
|
|
1197
|
+
const remove = this.bridge.data(first, derivedType.requestDelete);
|
|
1198
|
+
let changed = false;
|
|
1199
|
+
for (const bo of entities) {
|
|
1200
|
+
const alias = abapName(bo.behavior.alias);
|
|
1201
|
+
const { created, updated, deleted } = this.changes(bo);
|
|
1202
|
+
changed ||= created.length + updated.length + deleted.length > 0;
|
|
1203
|
+
for (const instance of created) {
|
|
1204
|
+
appendRow(component(create, alias), (row) => {
|
|
1205
|
+
this.bridge.fillRow(row, bo, instance.row);
|
|
1206
|
+
this.bridge.fillControl(row, bo, new Set([...instance.changed, ...bo.keys.map((k) => k.name)]));
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
// the rows hold every field (with full data), %control the changed ones
|
|
1210
|
+
for (const instance of updated) {
|
|
1211
|
+
appendRow(component(update, alias), (row) => {
|
|
1212
|
+
this.bridge.fillRow(row, bo, instance.row);
|
|
1213
|
+
this.bridge.fillControl(row, bo, instance.changed);
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
for (const instance of deleted) {
|
|
1217
|
+
appendRow(component(remove, alias), (row) => this.bridge.fillKey(row, bo, keyOf(bo, instance.persisted)));
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (!changed) {
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
const reported = this.bridge.response(first, "reported", true);
|
|
1224
|
+
const failed = saver.withFailed ? this.bridge.response(first, "failed", true) : undefined;
|
|
1225
|
+
this.inSaver = true;
|
|
1226
|
+
try {
|
|
1227
|
+
await this.bridge.call(saver.saveModified, { create, update, delete: remove, reported, ...(failed === undefined ? {} : { failed }) });
|
|
1228
|
+
if (saver.cleanup !== undefined) {
|
|
1229
|
+
await this.bridge.call(saver.cleanup, {});
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
finally {
|
|
1233
|
+
this.inSaver = false;
|
|
1234
|
+
}
|
|
1235
|
+
responses.add(await this.bridge.readResponses(first, failed, reported));
|
|
1236
|
+
}
|
|
1237
|
+
return responses;
|
|
1238
|
+
}
|
|
1239
|
+
async write() {
|
|
1240
|
+
for (const instance of this.instances.values()) {
|
|
1241
|
+
const { bo } = instance;
|
|
1242
|
+
if (bo.saving === "unmanaged") {
|
|
1243
|
+
// save_modified writes these instances
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
const columns = [...bo.columns.entries()];
|
|
1247
|
+
const key = keyOf(bo, instance.row);
|
|
1248
|
+
const where = (row) => sql.join(bo.keys.map((k) => sql `${sql.id(bo.columns.get(k.name) ?? abapName(k.name))} = ${toSql(k.type, row[k.name] ?? null)}`), sql ` AND `);
|
|
1249
|
+
if (instance.deleted) {
|
|
1250
|
+
if (instance.persisted !== undefined) {
|
|
1251
|
+
await sql `DELETE FROM ${sql.id(bo.table.name)} WHERE ${where(instance.persisted)}`.execute(this.db);
|
|
1252
|
+
}
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
if (instance.persisted === undefined) {
|
|
1256
|
+
const names = columns.map(([, c]) => sql.id(c));
|
|
1257
|
+
const values = columns.map(([f]) => toSql(this.bo.field(bo, f).type, instance.row[f] ?? null));
|
|
1258
|
+
await sql `INSERT INTO ${sql.id(bo.table.name)} (${sql.join(names)}) VALUES (${sql.join(values)})`.execute(this.db);
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
const touched = this.etagTouched.has(instance) ? bo.etagField : undefined;
|
|
1262
|
+
const changed = columns.filter(([f]) => (instance.changed.has(f) || f === touched) && !bo.keys.some((k) => k.name === f));
|
|
1263
|
+
if (changed.length === 0 && keyString(bo, instance.persisted) === keyString(bo, key)) {
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
const sets = (changed.length === 0 ? columns.filter(([f]) => !bo.keys.some((k) => k.name === f)) : changed).map(([f, c]) => sql `${sql.id(c)} = ${toSql(this.bo.field(bo, f).type, instance.row[f] ?? null)}`);
|
|
1267
|
+
if (sets.length > 0) {
|
|
1268
|
+
await sql `UPDATE ${sql.id(bo.table.name)} SET ${sql.join(sets)} WHERE ${where(instance.persisted)}`.execute(this.db);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
/** Keys of the instances created in the LUW, by content ID. */
|
|
1273
|
+
createdByCid(cid) {
|
|
1274
|
+
return this.cids.get(cid);
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* The instance whose ETag field covers an instance (of `bo`, with the values `row`): itself
|
|
1278
|
+
* for an `etag master`, the master reached by the `etag dependent by` associations otherwise.
|
|
1279
|
+
*/
|
|
1280
|
+
async etagMasterInstance(bo, row) {
|
|
1281
|
+
let current = bo;
|
|
1282
|
+
let values = row;
|
|
1283
|
+
for (let depth = 0; depth < 20; depth++) {
|
|
1284
|
+
const spec = current.behavior.etag;
|
|
1285
|
+
if (spec === undefined) {
|
|
1286
|
+
return undefined;
|
|
1287
|
+
}
|
|
1288
|
+
if (spec.kind === "master") {
|
|
1289
|
+
return this.instance(current, keyOf(current, values));
|
|
1290
|
+
}
|
|
1291
|
+
const association = this.bo.association(current, spec.by);
|
|
1292
|
+
if (association === undefined) {
|
|
1293
|
+
return undefined;
|
|
1294
|
+
}
|
|
1295
|
+
const targetKey = {};
|
|
1296
|
+
for (const pair of association.on) {
|
|
1297
|
+
const target = association.target.fields.find((f) => same(f.name, pair.target))?.name ?? pair.target;
|
|
1298
|
+
targetKey[target] = values[current.fields.find((f) => same(f.name, pair.source))?.name ?? pair.source] ?? null;
|
|
1299
|
+
}
|
|
1300
|
+
const next = await this.instance(association.target, keyOf(association.target, targetKey));
|
|
1301
|
+
if (next === undefined) {
|
|
1302
|
+
return undefined;
|
|
1303
|
+
}
|
|
1304
|
+
current = association.target;
|
|
1305
|
+
values = next.row;
|
|
1306
|
+
}
|
|
1307
|
+
return undefined;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* The ETag value an If-Match is compared with: the ETag field of the instance or of its ETag
|
|
1311
|
+
* master, as it was when the LUW began (the state the client has read: the requests of a change
|
|
1312
|
+
* set carry the ETags read before it, and the changes of the LUW, a determination that updates
|
|
1313
|
+
* the master for example, do not make them stale); undefined when the instance is not found.
|
|
1314
|
+
*/
|
|
1315
|
+
async etagValue(bo, key) {
|
|
1316
|
+
const instance = await this.instance(bo, keyOf(bo, key));
|
|
1317
|
+
if (instance === undefined) {
|
|
1318
|
+
return undefined;
|
|
1319
|
+
}
|
|
1320
|
+
const master = await this.etagMasterInstance(bo, instance.row);
|
|
1321
|
+
const field = master?.bo.etagField;
|
|
1322
|
+
return { value: master === undefined || field === undefined ? undefined : (master.persisted ?? master.row)[field] };
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* `etag dependent by` (D32): the ETag field of a master changes with its dependent instances.
|
|
1326
|
+
* The runtime sets it when it is an administrative time stamp it manages
|
|
1327
|
+
* (`localInstanceLastChangedAt`, `lastChangedAt`) and writes the master; at save time, so that
|
|
1328
|
+
* the requests of a change set check the ETag the client has read.
|
|
1329
|
+
*/
|
|
1330
|
+
async refreshEtagMasters() {
|
|
1331
|
+
const masters = new Set();
|
|
1332
|
+
for (const instance of [...this.instances.values()]) {
|
|
1333
|
+
if (instance.bo.behavior.etag?.kind !== "dependent") {
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
const created = instance.persisted === undefined;
|
|
1337
|
+
const changed = instance.deleted ? !created : created || instance.changed.size > 0;
|
|
1338
|
+
if (!changed) {
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
const master = await this.etagMasterInstance(instance.bo, instance.deleted ? (instance.persisted ?? instance.row) : instance.row);
|
|
1342
|
+
if (master !== undefined && !master.deleted) {
|
|
1343
|
+
masters.add(master);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
for (const master of masters) {
|
|
1347
|
+
const field = master.bo.etagField;
|
|
1348
|
+
if (field === undefined || master.bo.saving === "unmanaged" || (field !== master.bo.admin.localInstanceLastChangedAt && field !== master.bo.admin.lastChangedAt)) {
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
master.row[field] = this.services.now();
|
|
1352
|
+
this.etagTouched.add(master);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Instance and global feature control of rows about to be read (GET): __EntityControl… The
|
|
1357
|
+
* global and instance authorizations of the user disable the operations they refuse, as RAP
|
|
1358
|
+
* presents both to its consumers (D36).
|
|
1359
|
+
*/
|
|
1360
|
+
async features(bo, keys, operations, fields) {
|
|
1361
|
+
const mode = { local: false, privileged: false, external: true };
|
|
1362
|
+
const instance = await this.instanceFeatures(bo, keys, { operations, fields }, mode);
|
|
1363
|
+
const global = await this.globalFeatures(bo, operations, mode);
|
|
1364
|
+
for (const id of await this.globalAuthorization(bo, operations, mode)) {
|
|
1365
|
+
global.add(id);
|
|
1366
|
+
}
|
|
1367
|
+
for (const operation of operations) {
|
|
1368
|
+
for (const key of await this.instanceAuthorization(bo, keys, operation, mode)) {
|
|
1369
|
+
instance.disabled.add(`${key}|${operationId(operation)}`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return { ...instance, global };
|
|
1373
|
+
}
|
|
1374
|
+
/** The operations of an entity the user may perform: global authorization and global features (D36). */
|
|
1375
|
+
async allowed(bo, operations) {
|
|
1376
|
+
const mode = { local: false, privileged: false, external: true };
|
|
1377
|
+
const denied = await this.globalAuthorization(bo, operations, mode);
|
|
1378
|
+
const disabled = await this.globalFeatures(bo, operations, mode);
|
|
1379
|
+
return new Set(operations.map(operationId).filter((id) => !denied.has(id) && !disabled.has(id)));
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
export function operationId(op) {
|
|
1383
|
+
return op.kind === "action" || op.kind === "association" ? `${op.kind}:${op.name.toLowerCase()}` : op.kind;
|
|
1384
|
+
}
|
|
1385
|
+
//# sourceMappingURL=transaction.js.map
|