@open-rap/runtime 0.0.0-stage → 0.1.1
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,799 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes and operations of the OData service, on the managed RAP runtime: POST (create, deep
|
|
3
|
+
* insert, create through a navigation), PATCH and PUT, DELETE, bound and static actions,
|
|
4
|
+
* functions, change sets of $batch, If-Match, feature control of the rows read (enrich) and the
|
|
5
|
+
* translation of the RAP responses into OData errors and `sap-messages`.
|
|
6
|
+
*
|
|
7
|
+
* One request (or one change set) is one LUW: the operations, the determinations on modify after
|
|
8
|
+
* each request (D21), then the save; responses are rendered once the LUW is committed, from the
|
|
9
|
+
* projection views, so that they show what the database holds.
|
|
10
|
+
*/
|
|
11
|
+
import { baseEntityOf, baseFieldOf, ENTITY_CONTROL, entitySetOfEntity, FIELD_CONTROL, findEntity, OPERATION_CONTROL, same, } from "@open-rap/model";
|
|
12
|
+
import { operationId } from "../rap/transaction.js";
|
|
13
|
+
import { ODATA_MODE, RapRuntimeError, Responses } from "../rap/types.js";
|
|
14
|
+
import { fromJson, toJson } from "../values.js";
|
|
15
|
+
import { badRequest, methodNotAllowed, notFound, ODataError } from "./errors.js";
|
|
16
|
+
import { ChangesetFailure, errorResponse, jsonResponse, sapMessagesHeader, toPart } from "./handler.js";
|
|
17
|
+
import { etagOf, etagValue, JsonWriter } from "./json.js";
|
|
18
|
+
import { DraftRuntime } from "./draft.js";
|
|
19
|
+
/** A number of a request body, with its exact text. */
|
|
20
|
+
class JsonNumber {
|
|
21
|
+
text;
|
|
22
|
+
constructor(text) {
|
|
23
|
+
this.text = text;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function parseBody(body) {
|
|
27
|
+
if (body === undefined || body.trim() === "") {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(body, ((_key, value, context) => typeof value === "number" ? new JsonNumber(context?.source ?? String(value)) : value));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw badRequest("The request body is not valid JSON");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function jsonValue(edm, value) {
|
|
38
|
+
if (!(value instanceof JsonNumber)) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
return edm.type === "Edm.Decimal" || edm.type === "Edm.Int64" ? value.text : Number(value.text);
|
|
42
|
+
}
|
|
43
|
+
const STATUS = {
|
|
44
|
+
unspecific: 400,
|
|
45
|
+
unauthorized: 403,
|
|
46
|
+
not_found: 404,
|
|
47
|
+
locked: 409,
|
|
48
|
+
conflict: 409,
|
|
49
|
+
readonly: 400,
|
|
50
|
+
disabled: 409,
|
|
51
|
+
dependency: 424,
|
|
52
|
+
};
|
|
53
|
+
const SEVERITY = { E: "error", W: "warning", I: "info", S: "success", " ": "info" };
|
|
54
|
+
export class RapWriteHandler {
|
|
55
|
+
service;
|
|
56
|
+
read;
|
|
57
|
+
db;
|
|
58
|
+
rap;
|
|
59
|
+
sets = new Map();
|
|
60
|
+
json;
|
|
61
|
+
servicePath = "/";
|
|
62
|
+
/** The drafts of the draft BOs (D54). */
|
|
63
|
+
drafts;
|
|
64
|
+
constructor(service, read, db, rap) {
|
|
65
|
+
this.service = service;
|
|
66
|
+
this.read = read;
|
|
67
|
+
this.db = db;
|
|
68
|
+
this.rap = rap;
|
|
69
|
+
this.json = new JsonWriter(service, read);
|
|
70
|
+
const model = service.model;
|
|
71
|
+
for (const info of service.sets()) {
|
|
72
|
+
const base = baseEntityOf(model, info.entity);
|
|
73
|
+
const bo = rap.bo.find(base.name);
|
|
74
|
+
if (bo === undefined) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const toBase = new Map();
|
|
78
|
+
const toProperty = new Map();
|
|
79
|
+
for (const [name, { field }] of info.properties) {
|
|
80
|
+
const baseField = baseFieldOf(model, info.entity, field)?.name ?? field.name;
|
|
81
|
+
toBase.set(name, baseField);
|
|
82
|
+
toProperty.set(baseField, name);
|
|
83
|
+
}
|
|
84
|
+
this.sets.set(info.set.name, { info, bo, toBase, toProperty });
|
|
85
|
+
}
|
|
86
|
+
this.drafts = [...this.sets.values()].some((s) => s.info.set.draft !== undefined) ? new DraftRuntime(this) : undefined;
|
|
87
|
+
}
|
|
88
|
+
setServicePath(path) {
|
|
89
|
+
this.servicePath = path;
|
|
90
|
+
}
|
|
91
|
+
setInfo(info) {
|
|
92
|
+
const set = this.sets.get(info.set.name);
|
|
93
|
+
if (set === undefined) {
|
|
94
|
+
throw methodNotAllowed(`${info.set.name} is read-only`);
|
|
95
|
+
}
|
|
96
|
+
return set;
|
|
97
|
+
}
|
|
98
|
+
/** The entity set exposing a base entity. */
|
|
99
|
+
setOfBase(entity) {
|
|
100
|
+
return [...this.sets.values()].find((s) => same(s.bo.entity.name, entity));
|
|
101
|
+
}
|
|
102
|
+
baseKey(set, key) {
|
|
103
|
+
const row = {};
|
|
104
|
+
for (const [property, value] of Object.entries(key)) {
|
|
105
|
+
if (set.info.properties.get(property)?.field.draft === true) {
|
|
106
|
+
continue; // IsActiveEntity (D54): not a field of the business object
|
|
107
|
+
}
|
|
108
|
+
row[set.toBase.get(property) ?? property] = value;
|
|
109
|
+
}
|
|
110
|
+
return row;
|
|
111
|
+
}
|
|
112
|
+
projectedKey(set, key) {
|
|
113
|
+
const result = {};
|
|
114
|
+
for (const name of set.info.type.keys) {
|
|
115
|
+
// IsActiveEntity (D54): an instance of the business object is active, unless the key says so
|
|
116
|
+
result[name] = set.info.properties.get(name)?.field.draft === true ? (key[name] ?? true) : (key[set.toBase.get(name) ?? name] ?? null);
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
path(set, key) {
|
|
121
|
+
return `${set.info.set.name}${this.service.keyPredicate(set.info, key)}`;
|
|
122
|
+
}
|
|
123
|
+
// -------------------------------------------------------------------------------------------
|
|
124
|
+
// WriteHandler
|
|
125
|
+
async handle(request, target, ctx) {
|
|
126
|
+
const changeset = ctx.changeset;
|
|
127
|
+
if (changeset !== undefined) {
|
|
128
|
+
const outcome = await this.execute(changeset.tx, request, target, ctx);
|
|
129
|
+
return { status: outcome.status, headers: {}, deferred: () => this.render(outcome, ctx) };
|
|
130
|
+
}
|
|
131
|
+
const outcome = await this.luw(ctx, async (tx) => {
|
|
132
|
+
const result = await this.execute(tx, request, target, ctx);
|
|
133
|
+
if (request.method !== "GET") {
|
|
134
|
+
const resource = result.entity === undefined ? undefined : { set: result.entity.set, key: this.baseKey(result.entity.set, result.entity.key) };
|
|
135
|
+
result.messages.push(...(await this.save(tx, resource)));
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
});
|
|
139
|
+
return this.render(outcome, ctx);
|
|
140
|
+
}
|
|
141
|
+
async changeset(run, ctx) {
|
|
142
|
+
let responses;
|
|
143
|
+
let saveMessages = [];
|
|
144
|
+
try {
|
|
145
|
+
responses = await this.luw(ctx, async (tx) => {
|
|
146
|
+
const changesetCtx = { ...ctx, changeset: { tx }, contentIds: ctx.contentIds ?? new Map() };
|
|
147
|
+
const result = await run(changesetCtx);
|
|
148
|
+
saveMessages = await this.save(tx);
|
|
149
|
+
return result;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error instanceof ChangesetFailure) {
|
|
154
|
+
return error.response;
|
|
155
|
+
}
|
|
156
|
+
return errorResponse(error, ctx.ieee754);
|
|
157
|
+
}
|
|
158
|
+
const parts = [];
|
|
159
|
+
for (const [i, { response, contentId }] of responses.entries()) {
|
|
160
|
+
let rendered = response.deferred === undefined ? response : await response.deferred();
|
|
161
|
+
if (i === responses.length - 1 && saveMessages.length > 0) {
|
|
162
|
+
rendered = { ...rendered, headers: { ...rendered.headers, ...sapMessagesHeader([...this.messagesOf(rendered), ...saveMessages]) } };
|
|
163
|
+
}
|
|
164
|
+
parts.push(toPart(rendered, contentId));
|
|
165
|
+
}
|
|
166
|
+
return parts;
|
|
167
|
+
}
|
|
168
|
+
messagesOf(response) {
|
|
169
|
+
const header = response.headers["sap-messages"];
|
|
170
|
+
if (header === undefined) {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
const numeric = { 1: "success", 2: "info", 3: "warning", 4: "error" };
|
|
174
|
+
return JSON.parse(header).map((m) => ({
|
|
175
|
+
code: m.code,
|
|
176
|
+
message: m.message,
|
|
177
|
+
severity: numeric[m.numericSeverity] ?? "info",
|
|
178
|
+
...(m.target === undefined ? {} : { target: m.target }),
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
luw(ctx, fn) {
|
|
182
|
+
return this.rap.transaction(this.db, ctx.user, ctx.language, async (tx) => {
|
|
183
|
+
try {
|
|
184
|
+
return await fn(tx);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (error instanceof RapRuntimeError) {
|
|
188
|
+
throw new ODataError(400, "RAP_RUNTIME_ERROR", error.message);
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/** The save sequence; an error (and a rollback) when it fails. Returns the other messages. */
|
|
195
|
+
async save(tx, resource) {
|
|
196
|
+
const responses = await tx.save();
|
|
197
|
+
if (responses.hasFailed) {
|
|
198
|
+
throw this.error(responses, resource);
|
|
199
|
+
}
|
|
200
|
+
return this.messages(responses.reported, resource);
|
|
201
|
+
}
|
|
202
|
+
// -------------------------------------------------------------------------------------------
|
|
203
|
+
// requests
|
|
204
|
+
async execute(tx, request, target, ctx) {
|
|
205
|
+
const { segments, options } = target;
|
|
206
|
+
const last = segments[segments.length - 1];
|
|
207
|
+
const minimal = /return=minimal/i.test(request.headers["prefer"] ?? "");
|
|
208
|
+
const outcome = (status) => ({ status, messages: [], options, minimal });
|
|
209
|
+
// drafts and the actions of the draft (D54); active instances go on below
|
|
210
|
+
const draft = this.drafts === undefined ? undefined : await this.drafts.route(tx, request, target, ctx, minimal);
|
|
211
|
+
if (draft !== undefined) {
|
|
212
|
+
return draft;
|
|
213
|
+
}
|
|
214
|
+
const resolved = await this.resolve(tx, segments);
|
|
215
|
+
if (last?.kind === "operation") {
|
|
216
|
+
return this.operation(tx, request, resolved, last.name, last.parameters ?? [], options, ctx, minimal);
|
|
217
|
+
}
|
|
218
|
+
const method = request.method;
|
|
219
|
+
if (method === "POST") {
|
|
220
|
+
if (resolved.key !== undefined && resolved.navigation === undefined) {
|
|
221
|
+
throw methodNotAllowed("POST on an entity: use PATCH, or POST on its collection");
|
|
222
|
+
}
|
|
223
|
+
const body = parseBody(request.body);
|
|
224
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
225
|
+
throw badRequest("The request body must be a JSON object");
|
|
226
|
+
}
|
|
227
|
+
let created;
|
|
228
|
+
if (resolved.navigation !== undefined && resolved.parent !== undefined) {
|
|
229
|
+
// POST Travel(…)/_Booking: create by association
|
|
230
|
+
const { parent, navigation } = resolved;
|
|
231
|
+
const nav = parent.set.info.navigations.get(navigation);
|
|
232
|
+
if (!nav.insertable) {
|
|
233
|
+
throw methodNotAllowed(`${parent.set.info.set.name}/${navigation}: creation through this navigation is not allowed`);
|
|
234
|
+
}
|
|
235
|
+
created = await this.createBy(tx, parent.set, this.baseKey(parent.set, parent.key), navigation, body);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
const set = resolved.set;
|
|
239
|
+
if (!set.info.set.insertable) {
|
|
240
|
+
throw methodNotAllowed(`${set.info.set.name} does not allow creation`);
|
|
241
|
+
}
|
|
242
|
+
created = await this.createDeep(tx, set, body);
|
|
243
|
+
}
|
|
244
|
+
await this.determine(tx, created.responses);
|
|
245
|
+
const key = this.projectedKey(created.set, created.key);
|
|
246
|
+
if (request.contentId !== undefined && ctx.contentIds !== undefined) {
|
|
247
|
+
ctx.contentIds.set(request.contentId, this.path(created.set, key));
|
|
248
|
+
}
|
|
249
|
+
return { ...outcome(minimal ? 204 : 201), entity: { set: created.set, key }, location: this.path(created.set, key), messages: this.messages(created.responses.reported, undefined) };
|
|
250
|
+
}
|
|
251
|
+
if (resolved.key === undefined) {
|
|
252
|
+
throw methodNotAllowed(`${method} needs a single entity`);
|
|
253
|
+
}
|
|
254
|
+
const set = resolved.set;
|
|
255
|
+
const key = this.baseKey(set, resolved.key);
|
|
256
|
+
if (method === "PATCH" || method === "PUT") {
|
|
257
|
+
if (!set.info.set.updatable) {
|
|
258
|
+
throw methodNotAllowed(`${set.info.set.name} does not allow updates`);
|
|
259
|
+
}
|
|
260
|
+
await this.checkEtag(tx, request, set, key, true);
|
|
261
|
+
const body = parseBody(request.body);
|
|
262
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
263
|
+
throw badRequest("The request body must be a JSON object");
|
|
264
|
+
}
|
|
265
|
+
const { values, fields } = this.values(set, body, method === "PUT");
|
|
266
|
+
for (const k of set.bo.keys) {
|
|
267
|
+
if (fields.has(k.name)) {
|
|
268
|
+
if (values[k.name] !== key[k.name]) {
|
|
269
|
+
throw badRequest(`The key property ${set.toProperty.get(k.name) ?? k.name} cannot be changed`, set.toProperty.get(k.name));
|
|
270
|
+
}
|
|
271
|
+
fields.delete(k.name);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const responses = await tx.update(set.bo, [{ key, values, fields }], ODATA_MODE);
|
|
275
|
+
this.failIf(responses, set, resolved.key);
|
|
276
|
+
await this.determine(tx, responses);
|
|
277
|
+
return { ...outcome(minimal ? 204 : 200), entity: { set, key: resolved.key }, messages: this.messages(responses.reported, { set, key }) };
|
|
278
|
+
}
|
|
279
|
+
if (method === "DELETE") {
|
|
280
|
+
if (!set.info.set.deletable) {
|
|
281
|
+
throw methodNotAllowed(`${set.info.set.name} does not allow deletion`);
|
|
282
|
+
}
|
|
283
|
+
await this.checkEtag(tx, request, set, key, true);
|
|
284
|
+
const responses = await tx.delete(set.bo, [{ key }], ODATA_MODE);
|
|
285
|
+
this.failIf(responses, set, resolved.key);
|
|
286
|
+
await this.determine(tx, responses);
|
|
287
|
+
return { ...outcome(204), messages: this.messages(responses.reported, { set, key }) };
|
|
288
|
+
}
|
|
289
|
+
throw methodNotAllowed(`${method} is not supported`);
|
|
290
|
+
}
|
|
291
|
+
/** Determinations on modify at the end of a request (D21). */
|
|
292
|
+
async determine(tx, previous) {
|
|
293
|
+
const responses = await tx.determineOnModify();
|
|
294
|
+
previous.reported.push(...responses.reported);
|
|
295
|
+
if (responses.hasFailed) {
|
|
296
|
+
throw this.error(responses, undefined);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* The entity set, key, and the navigation of a write's resource path. Navigations are followed
|
|
301
|
+
* in the LUW, which sees the instances created earlier in the change set (`$1/_Booking`).
|
|
302
|
+
*/
|
|
303
|
+
async resolve(tx, segments) {
|
|
304
|
+
const [first, ...rest] = segments;
|
|
305
|
+
if (first === undefined || first.kind !== "entitySet") {
|
|
306
|
+
throw badRequest("The resource path must start with an entity set");
|
|
307
|
+
}
|
|
308
|
+
let set = this.setInfo(this.service.set(first.name));
|
|
309
|
+
let key = first.key === undefined ? undefined : this.service.key(set.info, first.key);
|
|
310
|
+
let parent;
|
|
311
|
+
let navigation;
|
|
312
|
+
for (const segment of rest) {
|
|
313
|
+
if (segment.kind === "operation") {
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
if (segment.kind !== "navigation") {
|
|
317
|
+
throw badRequest(`Writes are not supported on ${segment.kind} segments`);
|
|
318
|
+
}
|
|
319
|
+
if (key === undefined) {
|
|
320
|
+
throw badRequest(`${segment.name}: navigation needs a single entity`);
|
|
321
|
+
}
|
|
322
|
+
const { navigation: nav, target } = this.service.navigationTarget(set.info, segment.name);
|
|
323
|
+
const targetSet = this.setInfo(target);
|
|
324
|
+
const association = this.associationOf(set, segment.name);
|
|
325
|
+
const { results, responses } = await tx.readByAssociation(set.bo, association, [this.baseKey(set, key)]);
|
|
326
|
+
if (responses.hasFailed) {
|
|
327
|
+
throw notFound(this.path(set, key));
|
|
328
|
+
}
|
|
329
|
+
if (segment.key !== undefined || !nav.collection) {
|
|
330
|
+
const wanted = segment.key === undefined ? undefined : this.service.key(target, segment.key);
|
|
331
|
+
const found = results.map((r) => this.projectedKey(targetSet, r.target)).find((k) => wanted === undefined || target.type.keys.every((name) => k[name] === wanted[name]));
|
|
332
|
+
if (found === undefined) {
|
|
333
|
+
throw notFound(wanted === undefined ? `${this.path(set, key)}/${segment.name}` : this.path(targetSet, wanted));
|
|
334
|
+
}
|
|
335
|
+
set = targetSet;
|
|
336
|
+
key = found;
|
|
337
|
+
navigation = undefined;
|
|
338
|
+
parent = undefined;
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
parent = { set, key };
|
|
342
|
+
set = targetSet;
|
|
343
|
+
key = undefined;
|
|
344
|
+
navigation = segment.name;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const result = { set };
|
|
348
|
+
if (key !== undefined)
|
|
349
|
+
result.key = key;
|
|
350
|
+
if (navigation !== undefined)
|
|
351
|
+
result.navigation = navigation;
|
|
352
|
+
if (parent !== undefined)
|
|
353
|
+
result.parent = parent;
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
/** Properties of a request body → base values and the fields given. */
|
|
357
|
+
values(set, body, replace) {
|
|
358
|
+
const values = {};
|
|
359
|
+
const fields = new Set();
|
|
360
|
+
const navigations = new Map();
|
|
361
|
+
for (const [name, value] of Object.entries(body)) {
|
|
362
|
+
if (name.includes("@")) {
|
|
363
|
+
continue; // @odata.type, @odata.etag…
|
|
364
|
+
}
|
|
365
|
+
if (name.startsWith("__")) {
|
|
366
|
+
continue; // instance feature control sent back by clients
|
|
367
|
+
}
|
|
368
|
+
const property = set.info.properties.get(name);
|
|
369
|
+
if (property?.field.draft === true) {
|
|
370
|
+
continue; // IsActiveEntity, HasDraftEntity… sent back by clients (D54)
|
|
371
|
+
}
|
|
372
|
+
if (property?.property.calculated === true) {
|
|
373
|
+
continue; // a calculated element (D67) is computed: its value in a body is ignored (Core.Computed)
|
|
374
|
+
}
|
|
375
|
+
if (property === undefined) {
|
|
376
|
+
if (set.info.navigations.has(name)) {
|
|
377
|
+
navigations.set(name, value);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
throw badRequest(`${set.info.type.name} has no property ${name}`, name);
|
|
381
|
+
}
|
|
382
|
+
const base = set.toBase.get(name) ?? name;
|
|
383
|
+
values[base] = fromJson(property.field.type, property.property.edm, jsonValue(property.property.edm, value), name);
|
|
384
|
+
fields.add(base);
|
|
385
|
+
}
|
|
386
|
+
if (replace) {
|
|
387
|
+
for (const [name, property] of set.info.properties) {
|
|
388
|
+
const base = set.toBase.get(name) ?? name;
|
|
389
|
+
if (!fields.has(base) && !property.property.key && !property.property.computed) {
|
|
390
|
+
values[base] = fromJson(property.field.type, property.property.edm, null, name);
|
|
391
|
+
fields.add(base);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return { values, fields, navigations };
|
|
396
|
+
}
|
|
397
|
+
async createDeep(tx, set, body) {
|
|
398
|
+
const { values, fields, navigations } = this.values(set, body, false);
|
|
399
|
+
const cid = "%ODATA-ROOT";
|
|
400
|
+
const responses = await tx.create(set.bo, [{ cid, values, fields }], ODATA_MODE);
|
|
401
|
+
this.failIf(responses, set, undefined);
|
|
402
|
+
const created = tx.createdByCid(cid);
|
|
403
|
+
if (created === undefined) {
|
|
404
|
+
throw new ODataError(500, "INTERNAL_ERROR", "the created instance has no key");
|
|
405
|
+
}
|
|
406
|
+
await this.createChildren(tx, set, cid, navigations, responses);
|
|
407
|
+
return { set, key: created.key, responses };
|
|
408
|
+
}
|
|
409
|
+
/** Deep insert: the children given inline, created by association with %cid_ref. */
|
|
410
|
+
async createChildren(tx, set, cid, navigations, responses) {
|
|
411
|
+
let n = 0;
|
|
412
|
+
for (const [name, value] of navigations) {
|
|
413
|
+
const navigation = set.info.navigations.get(name);
|
|
414
|
+
if (navigation === undefined || !navigation.insertable) {
|
|
415
|
+
throw badRequest(`${set.info.type.name}/${name}: deep insert is only possible through compositions that allow creation`, name);
|
|
416
|
+
}
|
|
417
|
+
const targetSet = this.setInfo(this.service.set(navigation.targetSet));
|
|
418
|
+
const association = this.associationOf(set, name);
|
|
419
|
+
const children = Array.isArray(value) ? value : [value];
|
|
420
|
+
const targets = [];
|
|
421
|
+
const nested = [];
|
|
422
|
+
for (const child of children) {
|
|
423
|
+
if (typeof child !== "object" || child === null) {
|
|
424
|
+
throw badRequest(`${name}: the children must be JSON objects`, name);
|
|
425
|
+
}
|
|
426
|
+
const converted = this.values(targetSet, child, false);
|
|
427
|
+
const childCid = `${cid}/${name}/${n++}`;
|
|
428
|
+
targets.push({ cid: childCid, values: converted.values, fields: converted.fields });
|
|
429
|
+
nested.push({ cid: childCid, navigations: converted.navigations });
|
|
430
|
+
}
|
|
431
|
+
const created = await tx.createByAssociation(set.bo, association, [{ key: {}, cidRef: cid, targets }], ODATA_MODE);
|
|
432
|
+
this.failIf(created, targetSet, undefined);
|
|
433
|
+
responses.add(created);
|
|
434
|
+
for (const child of nested) {
|
|
435
|
+
if (child.navigations.size > 0) {
|
|
436
|
+
await this.createChildren(tx, targetSet, child.cid, child.navigations, responses);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
associationOf(set, navigation) {
|
|
442
|
+
const projected = set.info.entity.associations.find((a) => a.name === navigation);
|
|
443
|
+
const association = this.rap.bo.association(set.bo, projected?.baseAssociation ?? navigation);
|
|
444
|
+
if (association === undefined) {
|
|
445
|
+
throw badRequest(`${navigation} is not an association of the business object`);
|
|
446
|
+
}
|
|
447
|
+
return association;
|
|
448
|
+
}
|
|
449
|
+
async createBy(tx, parent, parentKey, navigation, body) {
|
|
450
|
+
const nav = parent.info.navigations.get(navigation);
|
|
451
|
+
const targetSet = this.setInfo(this.service.set(nav?.targetSet ?? ""));
|
|
452
|
+
const association = this.associationOf(parent, navigation);
|
|
453
|
+
const { values, fields, navigations } = this.values(targetSet, body, false);
|
|
454
|
+
const cid = "%ODATA-CHILD";
|
|
455
|
+
const responses = await tx.createByAssociation(parent.bo, association, [{ key: parentKey, targets: [{ cid, values, fields }] }], ODATA_MODE);
|
|
456
|
+
this.failIf(responses, parent, this.projectedKey(parent, parentKey));
|
|
457
|
+
const created = tx.createdByCid(cid);
|
|
458
|
+
if (created === undefined) {
|
|
459
|
+
throw new ODataError(500, "INTERNAL_ERROR", "the created instance has no key");
|
|
460
|
+
}
|
|
461
|
+
await this.createChildren(tx, targetSet, cid, navigations, responses);
|
|
462
|
+
return { set: targetSet, key: created.key, responses };
|
|
463
|
+
}
|
|
464
|
+
/** If-Match against the current ETag of the instance (in the LUW). */
|
|
465
|
+
async checkEtag(tx, request, set, key, required) {
|
|
466
|
+
const etagProperty = set.info.type.etag;
|
|
467
|
+
if (etagProperty === undefined && set.info.type.etagPath === undefined) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const header = request.headers["if-match"];
|
|
471
|
+
if (header === undefined || header.trim() === "") {
|
|
472
|
+
if (required) {
|
|
473
|
+
throw new ODataError(428, "PRECONDITION_REQUIRED", `${set.info.set.name}: the request needs an If-Match header`);
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (header.trim() === "*") {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
// the ETag field of the instance, or of its ETag master (etag dependent by, D32)
|
|
481
|
+
const current = await tx.etagValue(set.bo, key);
|
|
482
|
+
if (current === undefined) {
|
|
483
|
+
throw notFound(this.path(set, this.projectedKey(set, key)));
|
|
484
|
+
}
|
|
485
|
+
const etag = etagValue(current.value);
|
|
486
|
+
const given = header.split(",").map((t) => t.trim());
|
|
487
|
+
if (!given.includes(etag)) {
|
|
488
|
+
throw new ODataError(412, "PRECONDITION_FAILED", `${this.path(set, this.projectedKey(set, key))} was changed in the meantime: reload it`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// -------------------------------------------------------------------------------------------
|
|
492
|
+
// actions and functions
|
|
493
|
+
async operation(tx, request, resolved, qualifiedName, urlParameters, options, ctx, minimal) {
|
|
494
|
+
const set = resolved.set;
|
|
495
|
+
const name = qualifiedName.includes(".") ? qualifiedName.slice(qualifiedName.lastIndexOf(".") + 1) : qualifiedName;
|
|
496
|
+
const operation = this.service.operation(set.info, name, resolved.key === undefined);
|
|
497
|
+
const action = set.bo.behavior.actions.find((a) => same(a.name, operation.action));
|
|
498
|
+
if (action === undefined) {
|
|
499
|
+
throw badRequest(`${name} is not an operation of ${set.info.type.name}`);
|
|
500
|
+
}
|
|
501
|
+
if (operation.kind === "action" && request.method !== "POST") {
|
|
502
|
+
throw methodNotAllowed(`The action ${name} is called with POST`);
|
|
503
|
+
}
|
|
504
|
+
if (operation.kind === "function" && request.method !== "GET") {
|
|
505
|
+
throw methodNotAllowed(`The function ${name} is called with GET`);
|
|
506
|
+
}
|
|
507
|
+
const parameterEntity = action.parameter === undefined || action.parameter === "$self" ? undefined : findEntity(this.service.model, action.parameter);
|
|
508
|
+
const param = this.parameters(operation, parameterEntity?.fields ?? [], request, urlParameters);
|
|
509
|
+
const key = resolved.key === undefined ? undefined : this.baseKey(set, resolved.key);
|
|
510
|
+
if (key !== undefined && operation.kind === "action") {
|
|
511
|
+
await this.checkEtag(tx, request, set, key, false);
|
|
512
|
+
}
|
|
513
|
+
const cid = "%ODATA-ACTION";
|
|
514
|
+
const { results, responses } = await tx.execute(set.bo, action, [{ ...(key === undefined ? {} : { key }), ...(action.static || action.factory ? { cid } : {}), param }], ODATA_MODE);
|
|
515
|
+
this.failIf(responses, set, resolved.key);
|
|
516
|
+
if (operation.kind === "action") {
|
|
517
|
+
await this.determine(tx, responses);
|
|
518
|
+
}
|
|
519
|
+
const base = { status: 200, messages: this.messages(responses.reported, key === undefined ? undefined : { set, key }), options, minimal };
|
|
520
|
+
void ctx;
|
|
521
|
+
if (operation.returnType === undefined) {
|
|
522
|
+
return { ...base, status: 204 };
|
|
523
|
+
}
|
|
524
|
+
if (action.factory) {
|
|
525
|
+
const created = responses.mapped.find((m) => m.cid === cid);
|
|
526
|
+
const targetSet = created === undefined ? undefined : this.setOfBase(created.entity);
|
|
527
|
+
if (created === undefined || targetSet === undefined) {
|
|
528
|
+
throw new ODataError(500, "INTERNAL_ERROR", `the factory action ${name} did not map its %cid`);
|
|
529
|
+
}
|
|
530
|
+
return { ...base, entity: { set: targetSet, key: this.projectedKey(targetSet, created.key) } };
|
|
531
|
+
}
|
|
532
|
+
return this.actionResult(base, set, action.result?.entity ?? "$self", operation, results);
|
|
533
|
+
}
|
|
534
|
+
actionResult(base, set, resultEntity, operation, results) {
|
|
535
|
+
const returnType = operation.returnType;
|
|
536
|
+
if (returnType.entityType) {
|
|
537
|
+
const targetSet = resultEntity === "$self" ? set : this.setOf(resultEntity);
|
|
538
|
+
// instance actions: the key of the result row; static actions and other entities: the result itself
|
|
539
|
+
const entities = results
|
|
540
|
+
.map((r) => (resultEntity === "$self" && r.key !== undefined ? r.key : r.value))
|
|
541
|
+
.filter((k) => k !== undefined)
|
|
542
|
+
.map((k) => ({ set: targetSet, key: this.projectedKey(targetSet, k) }));
|
|
543
|
+
if (!returnType.collection) {
|
|
544
|
+
const entity = entities[0];
|
|
545
|
+
if (entity === undefined) {
|
|
546
|
+
return { ...base, status: 204 };
|
|
547
|
+
}
|
|
548
|
+
return { ...base, entity };
|
|
549
|
+
}
|
|
550
|
+
return { ...base, entities };
|
|
551
|
+
}
|
|
552
|
+
const complex = this.service.odata.complexTypes.find((c) => c.name === returnType.type);
|
|
553
|
+
const render = (value) => {
|
|
554
|
+
const object = {};
|
|
555
|
+
for (const p of complex?.properties ?? []) {
|
|
556
|
+
object[p.name] = toJson(p.edm, value?.[p.name] ?? null, false);
|
|
557
|
+
}
|
|
558
|
+
return object;
|
|
559
|
+
};
|
|
560
|
+
const values = results.map((r) => render(r.value));
|
|
561
|
+
const context = `$metadata#${this.service.namespace}.${returnType.type}`;
|
|
562
|
+
return { ...base, complex: { value: returnType.collection ? values : (values[0] ?? {}), context: returnType.collection ? `${context.replace("#", "#Collection(")})` : context } };
|
|
563
|
+
}
|
|
564
|
+
setOf(entity) {
|
|
565
|
+
const set = entitySetOfEntity(this.service.model, entity);
|
|
566
|
+
const info = set === undefined ? undefined : this.sets.get(set.name);
|
|
567
|
+
if (info === undefined) {
|
|
568
|
+
throw new ODataError(500, "INTERNAL_ERROR", `no entity set for ${entity}`);
|
|
569
|
+
}
|
|
570
|
+
return info;
|
|
571
|
+
}
|
|
572
|
+
parameters(operation, fields, request, urlParameters) {
|
|
573
|
+
const given = {};
|
|
574
|
+
if (operation.kind === "action") {
|
|
575
|
+
const body = parseBody(request.body);
|
|
576
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
577
|
+
throw badRequest("The action parameters must be a JSON object");
|
|
578
|
+
}
|
|
579
|
+
Object.assign(given, body);
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
for (const p of urlParameters) {
|
|
583
|
+
const value = p.value;
|
|
584
|
+
given[p.name] = value?.kind === "literal" ? value.literal?.value : undefined;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const param = {};
|
|
588
|
+
for (const [name, value] of Object.entries(given)) {
|
|
589
|
+
if (name.includes("@")) {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const p = operation.parameters.find((x) => x.name === name);
|
|
593
|
+
const field = fields.find((f) => f.name === name);
|
|
594
|
+
if (p === undefined || field === undefined) {
|
|
595
|
+
throw badRequest(`${operation.name} has no parameter ${name}`, name);
|
|
596
|
+
}
|
|
597
|
+
param[field.name] = fromJson(field.type, p.edm, jsonValue(p.edm, value), name);
|
|
598
|
+
}
|
|
599
|
+
return param;
|
|
600
|
+
}
|
|
601
|
+
// -------------------------------------------------------------------------------------------
|
|
602
|
+
// responses
|
|
603
|
+
failIf(responses, set, key) {
|
|
604
|
+
if (responses.hasFailed) {
|
|
605
|
+
throw this.error(responses, key === undefined ? undefined : { set, key: this.baseKey(set, key) });
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/** Messages of the RAP responses, with their OData targets. */
|
|
609
|
+
messages(reported, resource, severities = new Set(["S", "I", "W", " "])) {
|
|
610
|
+
return reported
|
|
611
|
+
.filter((r) => severities.has(r.message.severity))
|
|
612
|
+
.map((r) => {
|
|
613
|
+
const message = { code: r.message.msgid === undefined ? "RAP" : `${r.message.msgid}/${r.message.msgno ?? ""}`, message: r.message.text, severity: SEVERITY[r.message.severity] ?? "info" };
|
|
614
|
+
const target = this.target(r.entity, r.key, r.elements, resource);
|
|
615
|
+
if (target !== undefined) {
|
|
616
|
+
message.target = target;
|
|
617
|
+
}
|
|
618
|
+
return message;
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
/** `Field` for the resource of the request, `/Set(key)/Field` for another instance. */
|
|
622
|
+
target(entity, key, elements, resource) {
|
|
623
|
+
if (entity === undefined || key === undefined) {
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
const set = this.setOfBase(entity);
|
|
627
|
+
if (set === undefined) {
|
|
628
|
+
return undefined;
|
|
629
|
+
}
|
|
630
|
+
const property = elements.map((e) => set.toProperty.get(e) ?? set.info.navigations.get(e)?.name).find((p) => p !== undefined);
|
|
631
|
+
const own = resource !== undefined && resource.set === set && set.bo.keys.every((k) => resource.key[k.name] === key[k.name]);
|
|
632
|
+
if (own) {
|
|
633
|
+
return property;
|
|
634
|
+
}
|
|
635
|
+
const path = `/${this.path(set, this.projectedKey(set, key))}`;
|
|
636
|
+
return property === undefined ? path : `${path}/${property}`;
|
|
637
|
+
}
|
|
638
|
+
/** The OData error of failed RAP responses. */
|
|
639
|
+
error(responses, resource) {
|
|
640
|
+
const failed = responses.failed[0];
|
|
641
|
+
const errors = this.messages(responses.reported, resource, new Set(["E"]));
|
|
642
|
+
const others = this.messages(responses.reported, resource);
|
|
643
|
+
const status = STATUS[failed.cause] ?? 400;
|
|
644
|
+
if (errors.length === 0) {
|
|
645
|
+
errors.push({ code: `RAP/${failed.cause.toUpperCase()}`, message: this.genericMessage(failed), severity: "error" });
|
|
646
|
+
}
|
|
647
|
+
const [main, ...details] = [...errors, ...others];
|
|
648
|
+
const first = main;
|
|
649
|
+
return new ODataError(status, first.code, first.message, first.target, details.length === 0 ? [] : [first, ...details]);
|
|
650
|
+
}
|
|
651
|
+
genericMessage(failed) {
|
|
652
|
+
const set = this.setOfBase(failed.entity);
|
|
653
|
+
const instance = set === undefined || failed.key === undefined ? (set?.info.set.name ?? failed.entity) : this.path(set, this.projectedKey(set, failed.key));
|
|
654
|
+
const operation = failed.operation === undefined ? "" : failed.operation.kind === "action" ? `the action ${failed.operation.name}` : failed.operation.kind === "association" ? `the creation of ${failed.operation.name}` : `the ${failed.operation.kind}`;
|
|
655
|
+
switch (failed.cause) {
|
|
656
|
+
case "not_found":
|
|
657
|
+
return `${instance} not found`;
|
|
658
|
+
case "unauthorized":
|
|
659
|
+
return `You are not authorized to perform ${operation || "this operation"} on ${instance}`;
|
|
660
|
+
case "locked":
|
|
661
|
+
return `${instance} is locked by another user`;
|
|
662
|
+
case "conflict":
|
|
663
|
+
return `${instance} already exists`;
|
|
664
|
+
case "disabled":
|
|
665
|
+
return `${operation === "" ? "The operation" : operation[0]?.toUpperCase() + operation.slice(1)} is not possible for ${instance}`;
|
|
666
|
+
case "readonly":
|
|
667
|
+
return `${instance}: a read-only field cannot be changed`;
|
|
668
|
+
case "dependency":
|
|
669
|
+
return `${instance}: a related operation failed`;
|
|
670
|
+
default:
|
|
671
|
+
return `${operation === "" ? "The operation" : operation[0]?.toUpperCase() + operation.slice(1)} failed for ${instance}`;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
async render(outcome, ctx) {
|
|
675
|
+
const headers = { ...sapMessagesHeader(outcome.messages) };
|
|
676
|
+
if (outcome.location !== undefined) {
|
|
677
|
+
headers["Location"] = `${this.servicePath}${outcome.location}`;
|
|
678
|
+
headers["OData-EntityId"] = headers["Location"];
|
|
679
|
+
}
|
|
680
|
+
if (outcome.complex !== undefined) {
|
|
681
|
+
const body = Array.isArray(outcome.complex.value) ? { "@odata.context": outcome.complex.context, value: outcome.complex.value } : { "@odata.context": outcome.complex.context, ...outcome.complex.value };
|
|
682
|
+
return jsonResponse(200, body, ctx.ieee754, headers);
|
|
683
|
+
}
|
|
684
|
+
if (outcome.entities !== undefined) {
|
|
685
|
+
const rows = [];
|
|
686
|
+
for (const { set, key } of outcome.entities) {
|
|
687
|
+
const row = await this.read.readOne(set.info, key, outcome.options);
|
|
688
|
+
if (row !== undefined) {
|
|
689
|
+
await this.enrich(set.info, [row], outcome.options, ctx);
|
|
690
|
+
rows.push(this.json.entity(set.info, row, outcome.options, ctx));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const set = outcome.entities[0]?.set;
|
|
694
|
+
return jsonResponse(200, { "@odata.context": set === undefined ? "$metadata" : this.json.contextUrl(set.info, outcome.options, false), value: rows }, ctx.ieee754, headers);
|
|
695
|
+
}
|
|
696
|
+
if (outcome.entity === undefined || outcome.status === 204) {
|
|
697
|
+
return { status: 204, headers: { "OData-Version": "4.0", ...headers } };
|
|
698
|
+
}
|
|
699
|
+
const { set, key } = outcome.entity;
|
|
700
|
+
const row = await this.read.readOne(set.info, key, outcome.options);
|
|
701
|
+
if (row === undefined) {
|
|
702
|
+
return { status: 204, headers: { "OData-Version": "4.0", ...headers } };
|
|
703
|
+
}
|
|
704
|
+
await this.enrich(set.info, [row], outcome.options, ctx);
|
|
705
|
+
const etag = etagOf(set.info, row);
|
|
706
|
+
if (etag !== undefined) {
|
|
707
|
+
headers["ETag"] = etag;
|
|
708
|
+
}
|
|
709
|
+
const body = { "@odata.context": this.json.contextUrl(set.info, outcome.options, true), ...this.json.entity(set.info, row, outcome.options, ctx) };
|
|
710
|
+
return jsonResponse(outcome.status, body, ctx.ieee754, headers);
|
|
711
|
+
}
|
|
712
|
+
// -------------------------------------------------------------------------------------------
|
|
713
|
+
// permissions of the user (the UI shows the creation it may perform, D36)
|
|
714
|
+
/** Whether the user may create instances of each entity set that allows creation. */
|
|
715
|
+
async permissions(ctx) {
|
|
716
|
+
const result = {};
|
|
717
|
+
for (const [name, set] of this.sets) {
|
|
718
|
+
if (!set.info.set.insertable) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const allowed = await this.rap.transaction(this.db, ctx.user, ctx.language, (tx) => tx.allowed(set.bo, [{ kind: "create" }]));
|
|
722
|
+
result[name] = { create: allowed.has("create") };
|
|
723
|
+
}
|
|
724
|
+
return result;
|
|
725
|
+
}
|
|
726
|
+
// -------------------------------------------------------------------------------------------
|
|
727
|
+
// feature control of the rows read
|
|
728
|
+
async enrich(info, rowsRead, options, ctx) {
|
|
729
|
+
const set = this.sets.get(info.set.name);
|
|
730
|
+
const type = info.type;
|
|
731
|
+
let rows = rowsRead;
|
|
732
|
+
if (set === undefined || rows.length === 0) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const selected = (name) => options.select === undefined || options.select.some((p) => p[0] === "*" || p[0] === name);
|
|
736
|
+
const wantEntity = type.entityControl !== undefined && selected(ENTITY_CONTROL);
|
|
737
|
+
const wantOperations = type.operationControl.length > 0 && selected(OPERATION_CONTROL);
|
|
738
|
+
const wantFields = type.fieldControl.length > 0 && selected(FIELD_CONTROL);
|
|
739
|
+
if (!wantEntity && !wantOperations && !wantFields) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
// drafts (D54): edited by their user, actions after the activation
|
|
743
|
+
const drafts = rows.filter((row) => row["IsActiveEntity"] === false);
|
|
744
|
+
for (const row of drafts) {
|
|
745
|
+
if (wantEntity)
|
|
746
|
+
row[ENTITY_CONTROL] = { Updatable: true, Deletable: true };
|
|
747
|
+
if (wantOperations)
|
|
748
|
+
row[OPERATION_CONTROL] = Object.fromEntries(type.operationControl.map((name) => [name, info.navigations.has(name)]));
|
|
749
|
+
if (wantFields)
|
|
750
|
+
row[FIELD_CONTROL] = Object.fromEntries(type.fieldControl.map((p) => [p, type.properties.find((x) => x.name === p)?.mandatory === true ? 7 : 3]));
|
|
751
|
+
}
|
|
752
|
+
rows = rows.filter((row) => row["IsActiveEntity"] !== false);
|
|
753
|
+
if (rows.length === 0) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const keys = rows.map((row) => this.baseKey(set, Object.fromEntries(type.keys.map((k) => [k, (row[k] ?? null)]))));
|
|
757
|
+
const operations = [
|
|
758
|
+
...(type.entityControl?.updatable === true ? [{ kind: "update" }] : []),
|
|
759
|
+
...(type.entityControl?.deletable === true ? [{ kind: "delete" }] : []),
|
|
760
|
+
...type.operationControl.map((name) => {
|
|
761
|
+
const operation = this.service.odata.operations.find((o) => o.boundType === type.name && o.name === name);
|
|
762
|
+
if (operation !== undefined) {
|
|
763
|
+
return { kind: "action", name: operation.action };
|
|
764
|
+
}
|
|
765
|
+
const projected = info.entity.associations.find((a) => a.name === name);
|
|
766
|
+
return { kind: "association", name: projected?.baseAssociation ?? name };
|
|
767
|
+
}),
|
|
768
|
+
];
|
|
769
|
+
const fields = type.fieldControl.map((p) => set.toBase.get(p) ?? p);
|
|
770
|
+
const features = await this.rap.transaction(this.db, ctx.user, ctx.language, (tx) => tx.features(set.bo, keys, operations, fields));
|
|
771
|
+
rows.forEach((row, i) => {
|
|
772
|
+
const k = JSON.stringify(set.bo.keys.map((f) => keys[i]?.[f.name] ?? null));
|
|
773
|
+
const enabled = (op) => !features.global.has(operationId(op)) && !features.disabled.has(`${k}|${operationId(op)}`);
|
|
774
|
+
if (wantEntity) {
|
|
775
|
+
row[ENTITY_CONTROL] = {
|
|
776
|
+
Updatable: type.entityControl?.updatable === true ? enabled({ kind: "update" }) : true,
|
|
777
|
+
Deletable: type.entityControl?.deletable === true ? enabled({ kind: "delete" }) : true,
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
if (wantOperations) {
|
|
781
|
+
const control = {};
|
|
782
|
+
type.operationControl.forEach((name, j) => {
|
|
783
|
+
control[name] = enabled(operations[(type.entityControl?.updatable === true ? 1 : 0) + (type.entityControl?.deletable === true ? 1 : 0) + j]);
|
|
784
|
+
});
|
|
785
|
+
row[OPERATION_CONTROL] = control;
|
|
786
|
+
}
|
|
787
|
+
if (wantFields) {
|
|
788
|
+
const readonly = features.readonly.get(k);
|
|
789
|
+
const control = {};
|
|
790
|
+
for (const property of type.fieldControl) {
|
|
791
|
+
const mandatory = type.properties.find((p) => p.name === property)?.mandatory === true;
|
|
792
|
+
control[property] = readonly?.has(set.toBase.get(property) ?? property) === true ? 1 : mandatory ? 7 : 3;
|
|
793
|
+
}
|
|
794
|
+
row[FIELD_CONTROL] = control;
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
//# sourceMappingURL=write.js.map
|