biz-a-cli 2.3.80-15419 → 2.3.80
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/bin/app.js +20 -255
- package/bin/hub.js +0 -16
- package/bin/hubEvent.js +5 -98
- package/bin/tunnelTarget.js +0 -4
- package/engine/bpm/workflow.js +6 -7
- package/engine/cc/configValidate.js +0 -53
- package/engine/cc/guides/V2-SCREEN-AUTHORING.md +512 -531
- package/engine/cc/layer3Behavior.js +1 -2
- package/engine/cc/layer3Schema.js +1 -3
- package/engine/cc/livePipelines.js +3 -9
- package/engine/cc/publish.js +105 -86
- package/engine/domain/appConfig.js +18 -108
- package/engine/domain/applicationConfig.js +12 -87
- package/engine/domain/dialect.js +0 -138
- package/engine/domain/domainNavigation.js +50 -86
- package/engine/domain/entityConfigLoader.js +4 -16
- package/engine/domain/numericScale.js +0 -2
- package/engine/domain/system.js +0 -2
- package/engine/ext/publicTheme.js +1 -8
- package/engine/orm/apiRoute.js +61 -187
- package/engine/orm/entityConfig.js +2 -32
- package/engine/orm/finaAuth.js +0 -0
- package/engine/orm/finaEndpoint.js +0 -19
- package/engine/orm/imgQuery.js +0 -92
- package/engine/orm/listQuery.js +1 -6
- package/package.json +1 -1
- package/worker/cliScriptWorker.js +0 -5
- package/engine/attachments/store.js +0 -195
- package/engine/domain/domainRegistration.js +0 -65
- package/engine/fina/approval.js +0 -141
- package/engine/fina/approvalService.js +0 -59
- package/engine/fina/attachments.js +0 -165
- package/engine/fina/client.js +0 -84
- package/engine/fina/config.js +0 -26
- package/engine/fina/context.js +0 -58
- package/engine/fina/document.js +0 -67
- package/engine/fina/documentWrite.js +0 -173
- package/engine/fina/documents/index.js +0 -28
- package/engine/fina/documents/jv.js +0 -105
- package/engine/fina/documents/jvWrite.js +0 -260
- package/engine/fina/documents/si.js +0 -40
- package/engine/fina/errors.js +0 -17
- package/engine/fina/features.js +0 -86
- package/engine/fina/guard.js +0 -102
- package/engine/fina/identity.js +0 -87
- package/engine/fina/live.js +0 -19
- package/engine/fina/lock.js +0 -35
- package/engine/fina/master.js +0 -331
- package/engine/fina/masters/coa.js +0 -104
- package/engine/fina/masters/currency.js +0 -39
- package/engine/fina/masters/department.js +0 -32
- package/engine/fina/masters/index.js +0 -21
- package/engine/fina/masters/project.js +0 -53
- package/engine/fina/period.js +0 -56
- package/engine/fina/print.js +0 -136
- package/engine/fina/readPolicy.js +0 -67
- package/engine/fina/recurring.js +0 -200
- package/engine/fina/route.js +0 -61
- package/engine/fina/routes/attachment.js +0 -41
- package/engine/fina/routes/document.js +0 -66
- package/engine/fina/routes/master.js +0 -26
- package/engine/fina/routes/period.js +0 -24
- package/engine/fina/routes/print.js +0 -19
- package/engine/fina/routes/recurring.js +0 -27
- package/engine/fina/routes/session.js +0 -23
- package/engine/fina/routes/setup.js +0 -58
- package/engine/fina/routes/submission.js +0 -46
- package/engine/fina/routes/template.js +0 -31
- package/engine/fina/settings.js +0 -159
- package/engine/fina/sql.js +0 -34
- package/engine/fina/submission.js +0 -290
- package/engine/fina/submissionCompare.js +0 -171
- package/engine/fina/submissionRepo.js +0 -114
- package/engine/fina/template.js +0 -118
- package/engine/fina/userMap.js +0 -79
- package/engine/orm/companyLocale.js +0 -74
- package/engine/orm/userApiDataset.js +0 -153
- package/migrations/postgres/1790600000000__fina.sql +0 -33
- package/migrations/postgres/1790700000000__attachment.sql +0 -29
- package/migrations/postgres/1790800000000__fina_jv_submission.sql +0 -41
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 M6 (D11) — the PLATFORM attachment store: files belonging to a record, kept in the tenant database
|
|
3
|
-
* (SYS$ATTACHMENT, PostgreSQL; migrations/postgres/1790700000000__attachment.sql).
|
|
4
|
-
*
|
|
5
|
-
* App-agnostic. A file belongs to an OWNER `{ type, id }` (e.g. `fina.jv` / `140`), and every statement names it, so one
|
|
6
|
-
* record's files are never reached through another's. WHO may list, add or remove is the caller's rule (for FINA
|
|
7
|
-
* documents: engine/fina/attachments.js); the limits are the caller's too (`{ maxFiles, maxFileMb, extensions }`).
|
|
8
|
-
*
|
|
9
|
-
* Files travel in CHUNKS of at most 1 MiB, base64 in JSON: begin (name, size, the limits) → append … → finish (the
|
|
10
|
-
* stored length must be the declared size). One request per chunk keeps each call small on a phone connection; the
|
|
11
|
-
* engine path gives a socket call 15 s. Reads come back in the same chunks.
|
|
12
|
-
*
|
|
13
|
-
* ⚠️ NEVER LIVE CONTENT. The extension decides the stored type, never the browser, and only PDFs and raster images are
|
|
14
|
-
* typed as themselves: anything else (svg, html included) is application/octet-stream, so a browser asked to open it
|
|
15
|
-
* downloads it instead of running its script in the app's origin (`canOpen`).
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
export const CHUNK_BYTES = 1024 * 1024;
|
|
19
|
-
const TABLE = "SYS$ATTACHMENT";
|
|
20
|
-
const STALE = "interval '1 hour'";
|
|
21
|
-
|
|
22
|
-
const TYPES = { pdf: "application/pdf", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp" };
|
|
23
|
-
const OPENABLE = new Set(Object.values(TYPES));
|
|
24
|
-
|
|
25
|
-
export const extensionOf = (name) => {
|
|
26
|
-
const m = /\.([A-Za-z0-9]{1,10})$/.exec(String(name ?? ""));
|
|
27
|
-
return m ? m[1].toLowerCase() : null;
|
|
28
|
-
};
|
|
29
|
-
export const mimeOf = (name) => TYPES[extensionOf(name)] ?? "application/octet-stream";
|
|
30
|
-
export const canOpen = (mimeType) => OPENABLE.has(String(mimeType ?? ""));
|
|
31
|
-
|
|
32
|
-
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
33
|
-
const text = (value) => `'${String(value).replace(/'/g, "''")}'`;
|
|
34
|
-
const userOf = (userId) => (Number.isInteger(userId) ? String(userId) : "NULL");
|
|
35
|
-
|
|
36
|
-
const ownerOf = (owner) => {
|
|
37
|
-
const type = owner?.type;
|
|
38
|
-
const id = owner?.id === undefined || owner?.id === null ? "" : String(owner.id);
|
|
39
|
-
if (typeof type !== "string" || !/^[a-z0-9_.]{1,64}$/.test(type) || !/^[A-Za-z0-9_-]{1,64}$/.test(id)) {
|
|
40
|
-
throw fail("INVALID", "An attachment belongs to a record: an owner type and id of letters, digits, '.', '_' or '-'.");
|
|
41
|
-
}
|
|
42
|
-
return `OWNER_TYPE = ${text(type)} AND OWNER_ID = ${text(id)}`;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const idOf = (value) => {
|
|
46
|
-
const id = Number(value);
|
|
47
|
-
if (!Number.isInteger(id) || id <= 0) throw fail("INVALID", `An attachment id is a positive whole number, not ${JSON.stringify(value)}.`);
|
|
48
|
-
return id;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/* The name as a person gave it, without a path or control characters; at most 255 characters, the extension kept. */
|
|
52
|
-
const cleanName = (raw) => {
|
|
53
|
-
const base = String(raw ?? "")
|
|
54
|
-
.split(/[\\/]/)
|
|
55
|
-
.pop()
|
|
56
|
-
.replace(/[\u0000-\u001f\u007f]/g, "")
|
|
57
|
-
.trim();
|
|
58
|
-
if (!base || base === "." || base === "..") throw fail("INVALID", "A file needs a name.");
|
|
59
|
-
if (base.length <= 255) return base;
|
|
60
|
-
const ext = extensionOf(base);
|
|
61
|
-
return ext ? base.slice(0, 254 - ext.length) + "." + ext : base.slice(0, 255);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const limitsOf = (limits) => ({
|
|
65
|
-
maxFiles: Number.isInteger(limits?.maxFiles) ? limits.maxFiles : 0,
|
|
66
|
-
maxBytes: Math.floor(Number(limits?.maxFileMb ?? 0) * 1024 * 1024),
|
|
67
|
-
extensions: (Array.isArray(limits?.extensions) ? limits.extensions : []).map((e) => String(e).toLowerCase()),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const decodedLength = (b64) => (b64.length / 4) * 3 - (b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0);
|
|
71
|
-
|
|
72
|
-
export const createAttachmentStore = ({ exec }) => {
|
|
73
|
-
const count = async (where) => Number((await exec(`SELECT COUNT(*) AS N FROM ${TABLE} WHERE ${where}`))?.[0]?.n ?? 0);
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
/* The record's files (READY only), oldest first. */
|
|
77
|
-
list: async ({ owner }) => {
|
|
78
|
-
const where = ownerOf(owner);
|
|
79
|
-
const rows = (await exec(`SELECT ID, FILE_NAME, MIME_TYPE, SIZE_BYTES, CREATED_BY, CREATED_AT FROM ${TABLE} WHERE ${where} AND STATUS = 'READY' ORDER BY ID`)) ?? [];
|
|
80
|
-
return rows.map((r) => ({
|
|
81
|
-
id: Number(r.id),
|
|
82
|
-
fileName: r.file_name,
|
|
83
|
-
mimeType: r.mime_type,
|
|
84
|
-
size: Number(r.size_bytes),
|
|
85
|
-
createdBy: r.created_by === null || r.created_by === undefined ? null : Number(r.created_by),
|
|
86
|
-
createdAt: r.created_at,
|
|
87
|
-
}));
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
/* A new file, within the limits; answers the id its chunks go to. */
|
|
91
|
-
begin: async ({ owner, fileName, size, userId }, limits) => {
|
|
92
|
-
const where = ownerOf(owner);
|
|
93
|
-
const name = cleanName(fileName);
|
|
94
|
-
const { maxFiles, maxBytes, extensions } = limitsOf(limits);
|
|
95
|
-
const ext = extensionOf(name);
|
|
96
|
-
if (!ext || !extensions.includes(ext)) {
|
|
97
|
-
throw fail("ATTACHMENT_RULE", `${name}: this type of file is not allowed (allowed: ${extensions.join(", ") || "none"}).`);
|
|
98
|
-
}
|
|
99
|
-
const bytes = Number(size);
|
|
100
|
-
if (!Number.isInteger(bytes) || bytes <= 0) throw fail("ATTACHMENT_RULE", `${name} is empty.`);
|
|
101
|
-
if (bytes > maxBytes) throw fail("ATTACHMENT_RULE", `${name} is larger than ${limits?.maxFileMb} MB.`);
|
|
102
|
-
/* An upload left behind (the browser closed mid-way) is cleared before the count. */
|
|
103
|
-
await exec(`DELETE FROM ${TABLE} WHERE ${where} AND STATUS = 'UPLOADING' AND CREATED_AT < now() - ${STALE}`);
|
|
104
|
-
if ((await count(where)) + 1 > maxFiles) throw fail("ATTACHMENT_RULE", `At most ${maxFiles} files per document.`);
|
|
105
|
-
const rows = await exec(
|
|
106
|
-
`INSERT INTO ${TABLE} (OWNER_TYPE, OWNER_ID, FILE_NAME, MIME_TYPE, SIZE_BYTES, CONTENT, STATUS, CREATED_BY) ` +
|
|
107
|
-
`VALUES (${text(owner.type)}, ${text(String(owner.id))}, ${text(name)}, ${text(mimeOf(name))}, ${bytes}, '', 'UPLOADING', ${userOf(userId)}) RETURNING ID`,
|
|
108
|
-
);
|
|
109
|
-
return { id: Number(rows?.[0]?.id) };
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
/* One chunk (base64, ≤ 1 MiB) onto an open upload of that owner; never past the declared size. */
|
|
113
|
-
append: async ({ owner, id, chunk }) => {
|
|
114
|
-
const where = ownerOf(owner);
|
|
115
|
-
const target = idOf(id);
|
|
116
|
-
const b64 = typeof chunk === "string" ? chunk : "";
|
|
117
|
-
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(b64) || b64.length % 4 !== 0) throw fail("INVALID", "A chunk is base64.");
|
|
118
|
-
const bytes = decodedLength(b64);
|
|
119
|
-
if (bytes > CHUNK_BYTES) throw fail("ATTACHMENT_RULE", "A chunk is at most 1 MiB.");
|
|
120
|
-
const rows = await exec(
|
|
121
|
-
`UPDATE ${TABLE} SET CONTENT = CONTENT || decode('${b64}', 'base64') WHERE ID = ${target} AND ${where} ` +
|
|
122
|
-
`AND STATUS = 'UPLOADING' AND octet_length(CONTENT) + ${bytes} <= SIZE_BYTES RETURNING octet_length(CONTENT) AS STORED`,
|
|
123
|
-
);
|
|
124
|
-
if (!rows || rows.length === 0) throw fail("ATTACHMENT_RULE", "This upload is not open, or it is longer than its declared size.");
|
|
125
|
-
return { stored: Number(rows[0].stored) };
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
/* The upload complete: READY when every byte arrived; otherwise it is dropped. */
|
|
129
|
-
finish: async ({ owner, id }) => {
|
|
130
|
-
const where = ownerOf(owner);
|
|
131
|
-
const target = idOf(id);
|
|
132
|
-
const rows = await exec(
|
|
133
|
-
`UPDATE ${TABLE} SET STATUS = 'READY' WHERE ID = ${target} AND ${where} AND STATUS = 'UPLOADING' ` +
|
|
134
|
-
`AND octet_length(CONTENT) = SIZE_BYTES RETURNING ID, FILE_NAME, MIME_TYPE, SIZE_BYTES`,
|
|
135
|
-
);
|
|
136
|
-
if (!rows || rows.length === 0) {
|
|
137
|
-
await exec(`DELETE FROM ${TABLE} WHERE ID = ${target} AND ${where} AND STATUS = 'UPLOADING'`);
|
|
138
|
-
throw fail("ATTACHMENT_RULE", "The file did not arrive whole; send it again.");
|
|
139
|
-
}
|
|
140
|
-
const r = rows[0];
|
|
141
|
-
return { id: Number(r.id), fileName: r.file_name, mimeType: r.mime_type, size: Number(r.size_bytes) };
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
/* One chunk of a READY file of that owner, from `offset`. */
|
|
145
|
-
read: async ({ owner, id, offset = 0 }) => {
|
|
146
|
-
const where = ownerOf(owner);
|
|
147
|
-
const target = idOf(id);
|
|
148
|
-
const from = Number(offset);
|
|
149
|
-
if (!Number.isInteger(from) || from < 0) throw fail("INVALID", "The offset is a whole number from 0.");
|
|
150
|
-
const rows = await exec(
|
|
151
|
-
`SELECT translate(encode(substring(CONTENT from ${from + 1} for ${CHUNK_BYTES}), 'base64'), E'\\n', '') AS DATA, SIZE_BYTES, FILE_NAME, MIME_TYPE ` +
|
|
152
|
-
`FROM ${TABLE} WHERE ID = ${target} AND ${where} AND STATUS = 'READY'`,
|
|
153
|
-
);
|
|
154
|
-
if (!rows || rows.length === 0) throw fail("ATTACHMENT_NOT_FOUND", `Attachment ${target} was not found.`);
|
|
155
|
-
const r = rows[0];
|
|
156
|
-
const data = String(r.data ?? "");
|
|
157
|
-
const size = Number(r.size_bytes);
|
|
158
|
-
return { id: target, offset: from, size, done: from + decodedLength(data) >= size, data, fileName: r.file_name, mimeType: r.mime_type };
|
|
159
|
-
},
|
|
160
|
-
|
|
161
|
-
/* The ids named, of that owner only. */
|
|
162
|
-
remove: async ({ owner, ids }) => {
|
|
163
|
-
const where = ownerOf(owner);
|
|
164
|
-
const wanted = (Array.isArray(ids) ? ids : []).map(idOf);
|
|
165
|
-
if (wanted.length === 0) return { removed: [] };
|
|
166
|
-
const rows = (await exec(`DELETE FROM ${TABLE} WHERE ${where} AND ID IN (${wanted.join(", ")}) RETURNING ID`)) ?? [];
|
|
167
|
-
return { removed: rows.map((r) => Number(r.id)) };
|
|
168
|
-
},
|
|
169
|
-
|
|
170
|
-
/* Salin (L-11): the files named, as NEW files of another owner, within its count. */
|
|
171
|
-
copy: async ({ from, to, ids, userId }, limits) => {
|
|
172
|
-
const source = ownerOf(from);
|
|
173
|
-
const target = ownerOf(to);
|
|
174
|
-
const wanted = [...new Set((Array.isArray(ids) ? ids : []).map(idOf))];
|
|
175
|
-
if (wanted.length === 0) return { copied: [] };
|
|
176
|
-
if ((await count(`${source} AND STATUS = 'READY' AND ID IN (${wanted.join(", ")})`)) !== wanted.length) {
|
|
177
|
-
throw fail("ATTACHMENT_NOT_FOUND", "A file to copy is not among the source's attachments.");
|
|
178
|
-
}
|
|
179
|
-
const { maxFiles } = limitsOf(limits);
|
|
180
|
-
if ((await count(target)) + wanted.length > maxFiles) throw fail("ATTACHMENT_RULE", `At most ${maxFiles} files per document.`);
|
|
181
|
-
const rows =
|
|
182
|
-
(await exec(
|
|
183
|
-
`INSERT INTO ${TABLE} (OWNER_TYPE, OWNER_ID, FILE_NAME, MIME_TYPE, SIZE_BYTES, CONTENT, STATUS, CREATED_BY) ` +
|
|
184
|
-
`SELECT ${text(to.type)}, ${text(String(to.id))}, FILE_NAME, MIME_TYPE, SIZE_BYTES, CONTENT, 'READY', ${userOf(userId)} ` +
|
|
185
|
-
`FROM ${TABLE} WHERE ${source} AND STATUS = 'READY' AND ID IN (${wanted.join(", ")}) RETURNING ID`,
|
|
186
|
-
)) ?? [];
|
|
187
|
-
return { copied: rows.map((r) => Number(r.id)) };
|
|
188
|
-
},
|
|
189
|
-
|
|
190
|
-
/* Every file of a record that is gone. */
|
|
191
|
-
removeOwner: async ({ owner }) => {
|
|
192
|
-
await exec(`DELETE FROM ${TABLE} WHERE ${ownerOf(owner)}`);
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
};
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Registering domains in the tenant's APPLICATION_CONFIG — the one rule shared by the writers that ADD to it without
|
|
3
|
-
* owning it: Control Center's publish (engine/cc/publish.js) and `biza add --register` (bin/app.js, Doc 8).
|
|
4
|
-
*
|
|
5
|
-
* `domains[key].source = "<name>@<version>"`, where the key is the entry whose source already names that domain (an
|
|
6
|
-
* application may key a domain under another name), else the domain's own name (ruling 2026-09-30). Every other field
|
|
7
|
-
* of the entry, and every other domain, is kept.
|
|
8
|
-
*
|
|
9
|
-
* ⚠️ WHY THIS IS NOT A WHOLESALE WRITE. APPLICATION_CONFIG has several writers, and readers take the newest row. A
|
|
10
|
-
* writer that replaces it drops what the others added — their domains (and with them the entity configs the hub
|
|
11
|
-
* loads) and their menus.
|
|
12
|
-
*/
|
|
13
|
-
import { mergeDomainNavigation } from "./domainNavigation.js";
|
|
14
|
-
|
|
15
|
-
const SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
16
|
-
const isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
17
|
-
|
|
18
|
-
export const bumpPatch = (version) => {
|
|
19
|
-
const m = SEMVER.exec(String(version ?? ""));
|
|
20
|
-
return m ? `${m[1]}.${m[2]}.${Number(m[3]) + 1}` : "0.1.0";
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/* A domain's source "name@version" → name. */
|
|
24
|
-
export const sourceName = (source) => String(source ?? "").split("@")[0];
|
|
25
|
-
|
|
26
|
-
/* `versions`: a Map or a plain object, domain name → version. Answers a new config; the input is left as it was. */
|
|
27
|
-
export const registerDomainVersions = (applicationConfig, versions) => {
|
|
28
|
-
const app = JSON.parse(JSON.stringify(applicationConfig ?? {}));
|
|
29
|
-
app.domains = isObj(app.domains) ? app.domains : {};
|
|
30
|
-
const entries = versions instanceof Map ? [...versions] : Object.entries(versions ?? {});
|
|
31
|
-
for (const [name, version] of entries) {
|
|
32
|
-
const key = Object.keys(app.domains).find((k) => sourceName(app.domains[k]?.source) === name) ?? name;
|
|
33
|
-
app.domains[key] = { ...(app.domains[key] ?? {}), source: `${name}@${version}` };
|
|
34
|
-
}
|
|
35
|
-
return app;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/*
|
|
39
|
-
* Doc 8 (2026-10-10) — the inverse: take domains OUT of an application config (`biza unregister`), as when a domain
|
|
40
|
-
* moves from the host into its own application. `names`: domain names, matched against the entry key or its source.
|
|
41
|
-
*
|
|
42
|
-
* ⚠️ The menu leaves those domains' publishes stamped go with them (mergeDomainNavigation with no entries strips a
|
|
43
|
-
* domain's leaves and the groups they emptied). A leaf whose domain is no longer registered makes the next publish
|
|
44
|
-
* throw "Cannot resolve menu domain", so removing only the entry would break the host.
|
|
45
|
-
*
|
|
46
|
-
* Answers `{ config, removed }` (the removed entry keys); the input is left as it was.
|
|
47
|
-
*/
|
|
48
|
-
export const unregisterDomains = (applicationConfig, names) => {
|
|
49
|
-
const app = JSON.parse(JSON.stringify(applicationConfig ?? {}));
|
|
50
|
-
const wanted = new Set((names ?? []).map((n) => String(n ?? "").trim()).filter(Boolean));
|
|
51
|
-
const domains = isObj(app.domains) ? app.domains : {};
|
|
52
|
-
const removed = Object.keys(domains).filter((key) => wanted.has(key) || wanted.has(sourceName(domains[key]?.source)));
|
|
53
|
-
if (removed.length === 0) return { config: app, removed };
|
|
54
|
-
|
|
55
|
-
let next = app;
|
|
56
|
-
for (const key of removed) {
|
|
57
|
-
/* A leaf may name its domain by key or by source name (resolveDomainKeyByReference takes either). */
|
|
58
|
-
for (const reference of new Set([key, sourceName(domains[key]?.source)])) {
|
|
59
|
-
next = { ...next, navigation: mergeDomainNavigation({ applicationConfig: next, domainKey: reference, menus: [] }) };
|
|
60
|
-
}
|
|
61
|
-
delete domains[key];
|
|
62
|
-
}
|
|
63
|
-
next.domains = domains;
|
|
64
|
-
return { config: next, removed };
|
|
65
|
-
};
|
package/engine/fina/approval.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 M7 — the JV approval workflow FINA Cloud generates when Need Approval is on (A-02, A-03, A-04).
|
|
3
|
-
*
|
|
4
|
-
* ONE WAY: a submission waits (MENUNGGU) for the approver role (A-06, D13 FINA_JV_APPROVER, mapped to user groups in
|
|
5
|
-
* SYS$BPM_ROLE_GROUP); SETUJUI ends it approved and its effect posts the JV to FINA (A-07); TOLAK ends it rejected (A-08).
|
|
6
|
-
* Another flow (a revision loop) is the user's own work in the BPM module (A-03): a published definition that is not
|
|
7
|
-
* this one is KEPT, never overwritten.
|
|
8
|
-
*
|
|
9
|
-
* The effects are one-line CLI scripts that hand the instance to engine/fina (`lib.fina.approval`, submission.js): the
|
|
10
|
-
* posting, its rules and its user are the server's, never a script's.
|
|
11
|
-
*/
|
|
12
|
-
import { sqlString } from "./sql.js";
|
|
13
|
-
|
|
14
|
-
export const APPROVAL = Object.freeze({
|
|
15
|
-
key: "FINA_JV_APPROVAL",
|
|
16
|
-
entityType: "FINA_JV_SUBMISSION",
|
|
17
|
-
role: "FINA_JV_APPROVER",
|
|
18
|
-
waiting: "MENUNGGU",
|
|
19
|
-
approved: "DISETUJUI",
|
|
20
|
-
rejected: "DITOLAK",
|
|
21
|
-
postScript: "fina_jv_submission_post",
|
|
22
|
-
rejectScript: "fina_jv_submission_reject",
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const role = [{ roleCode: APPROVAL.role }];
|
|
26
|
-
|
|
27
|
-
export const approvalDefinition = () => ({
|
|
28
|
-
workflowKey: APPROVAL.key,
|
|
29
|
-
name: "Persetujuan Jurnal Voucher (FINA Cloud)",
|
|
30
|
-
entityType: APPROVAL.entityType,
|
|
31
|
-
description: "Generated by the FINA Cloud console when Need Approval is on (Doc 8 A-02).",
|
|
32
|
-
status: "DRAFT",
|
|
33
|
-
initialStateCode: APPROVAL.waiting,
|
|
34
|
-
states: [
|
|
35
|
-
{ stateCode: APPROVAL.waiting, label: "Menunggu persetujuan", taskEnabled: true, taskName: "Setujui Jurnal Voucher", assignmentType: "ROLE", assignmentValue: APPROVAL.role },
|
|
36
|
-
{ stateCode: APPROVAL.approved, label: "Disetujui" },
|
|
37
|
-
{ stateCode: APPROVAL.rejected, label: "Ditolak" },
|
|
38
|
-
],
|
|
39
|
-
transitions: [
|
|
40
|
-
{
|
|
41
|
-
transitionCode: "SETUJUI",
|
|
42
|
-
fromStateCode: APPROVAL.waiting,
|
|
43
|
-
toStateCode: APPROVAL.approved,
|
|
44
|
-
label: "Setujui",
|
|
45
|
-
roles: role,
|
|
46
|
-
effects: [{ effectType: "CLI_SCRIPT", eventName: APPROVAL.postScript }],
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
transitionCode: "TOLAK",
|
|
50
|
-
fromStateCode: APPROVAL.waiting,
|
|
51
|
-
toStateCode: APPROVAL.rejected,
|
|
52
|
-
label: "Tolak",
|
|
53
|
-
roles: role,
|
|
54
|
-
effects: [{ effectType: "CLI_SCRIPT", eventName: APPROVAL.rejectScript }],
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const script = (call) => `(function (lib) { return { functions: { onInit: async function (scriptData) { return ${call}(scriptData); } } }; })`;
|
|
60
|
-
|
|
61
|
-
export const approvalScripts = () => ({
|
|
62
|
-
[APPROVAL.postScript]: script("lib.fina.approval.post"),
|
|
63
|
-
[APPROVAL.rejectScript]: script("lib.fina.approval.reject"),
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
/* What makes two definitions the same workflow (the version, status and description aside). */
|
|
67
|
-
const shape = (def) => JSON.stringify({ entityType: def?.entityType, initialStateCode: def?.initialStateCode, states: def?.states, transitions: def?.transitions });
|
|
68
|
-
|
|
69
|
-
/*
|
|
70
|
-
* The console Publish with Need Approval on (route fina/setup/approval). `deps`: exec / execStatements on this hub's
|
|
71
|
-
* database, and the BPM engine's `save` / `publish` (engine/bpm/workflow.js).
|
|
72
|
-
* - The scripts are always refreshed (an upsert by name).
|
|
73
|
-
* - No published definition yet → this one, saved as a draft and published.
|
|
74
|
-
* - The same one published → nothing (no new version on every Publish).
|
|
75
|
-
* - Another one published (edited in the BPM module, A-03) → kept.
|
|
76
|
-
*/
|
|
77
|
-
export const publishApproval = async ({ exec, execStatements, save, publish, userId }) => {
|
|
78
|
-
const statements = [];
|
|
79
|
-
for (const [name, body] of Object.entries(approvalScripts())) {
|
|
80
|
-
statements.push(`UPDATE SYS$CLI_SCRIPT SET SCRIPT = ${sqlString(body)} WHERE SCRIPT_NAME = ${sqlString(name)}`);
|
|
81
|
-
statements.push(
|
|
82
|
-
`INSERT INTO SYS$CLI_SCRIPT (SCRIPT_NAME, SCRIPT) SELECT ${sqlString(name)}, ${sqlString(body)} ` +
|
|
83
|
-
`WHERE NOT EXISTS (SELECT 1 FROM SYS$CLI_SCRIPT WHERE SCRIPT_NAME = ${sqlString(name)})`,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
await execStatements(statements);
|
|
87
|
-
|
|
88
|
-
const latest =
|
|
89
|
-
(await exec(`SELECT DEFINITION_JSON FROM SYS$BPM_WORKFLOW_DEF WHERE WORKFLOW_KEY = ${sqlString(APPROVAL.key)} AND STATUS = 'PUBLISHED' ORDER BY VERSION DESC LIMIT 1`)) ?? [];
|
|
90
|
-
const definition = approvalDefinition();
|
|
91
|
-
if (latest.length > 0) {
|
|
92
|
-
let published = null;
|
|
93
|
-
try {
|
|
94
|
-
published = JSON.parse(latest[0].definition_json);
|
|
95
|
-
} catch {
|
|
96
|
-
published = null;
|
|
97
|
-
}
|
|
98
|
-
return shape(published) === shape(definition) ? { published: false } : { published: false, kept: true };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const rawJsonString = JSON.stringify(definition);
|
|
102
|
-
const saved = await save({ id: null, userId, rawJsonString });
|
|
103
|
-
const row = Array.isArray(saved) ? saved[0] : saved;
|
|
104
|
-
const id = row?.ID ?? row?.id;
|
|
105
|
-
if (id === undefined || id === null) throw new Error("The approval workflow was not saved: the BPM engine answered no id.");
|
|
106
|
-
await publish({ id, userId, rawJsonString });
|
|
107
|
-
return { published: true };
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const instanceIdOf = (value) => {
|
|
111
|
-
const id = Number(value);
|
|
112
|
-
if (!Number.isInteger(id) || id <= 0 || !/^\d+$/.test(String(value).trim())) throw new Error(`A workflow instance id is a positive whole number, not ${JSON.stringify(value)}.`);
|
|
113
|
-
return id;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
/*
|
|
117
|
-
* What submission.js needs of the BPM engine, on this hub's database (`dbIndex`): reads straight from SYS$BPM_*, and
|
|
118
|
-
* the runtime's own initiate / terminate (imported lazily, as bin/hubEvent.js does: it brings the worker pool).
|
|
119
|
-
*/
|
|
120
|
-
export const createApprovalBpm = ({ exec, dbIndex, runtime = () => import("../bpm/workflow-runtime.js") }) => ({
|
|
121
|
-
published: async () =>
|
|
122
|
-
((await exec(`SELECT 1 AS N FROM SYS$BPM_WORKFLOW_DEF WHERE WORKFLOW_KEY = ${sqlString(APPROVAL.key)} AND STATUS = 'PUBLISHED' LIMIT 1`)) ?? []).length > 0,
|
|
123
|
-
|
|
124
|
-
initiate: async ({ workflowKey, entityType, entityId, userId }) => {
|
|
125
|
-
const rows = await (await runtime()).initiate({ workflowKey, entityType, entityId, userId, apiConfig: { dbIndex } });
|
|
126
|
-
const instanceId = Number(rows?.[0]?.ID ?? rows?.[0]?.id);
|
|
127
|
-
if (!Number.isInteger(instanceId) || instanceId <= 0) throw new Error("The BPM engine answered no instance id.");
|
|
128
|
-
return { instanceId };
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
terminate: async ({ instanceId, userId }) => (await runtime()).terminate({ instanceId: instanceIdOf(instanceId), userId, apiConfig: { dbIndex } }),
|
|
132
|
-
|
|
133
|
-
instance: async (instanceId) =>
|
|
134
|
-
((await exec(`SELECT ENTITY_TYPE, ENTITY_ID, CURRENT_STATE_CODE, STATUS FROM SYS$BPM_WF_INSTANCE WHERE ID = ${instanceIdOf(instanceId)}`)) ?? [])[0] ?? null,
|
|
135
|
-
|
|
136
|
-
decider: async (instanceId) => {
|
|
137
|
-
const row = ((await exec(`SELECT ACTOR_ID FROM SYS$BPM_STATE_HISTORY WHERE SYS$BPM_WF_INSTANCE_ID = ${instanceIdOf(instanceId)} ORDER BY ID DESC LIMIT 1`)) ?? [])[0];
|
|
138
|
-
const id = Number(row?.actor_id);
|
|
139
|
-
return row?.actor_id === null || row?.actor_id === undefined || !Number.isInteger(id) ? null : id;
|
|
140
|
-
},
|
|
141
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 M7 — the JV submission service with its live parts, for the routes (routes/submission.js) and for the approval
|
|
3
|
-
* workflow's effects (`lib.fina`, worker/cliScriptWorker.js).
|
|
4
|
-
*
|
|
5
|
-
* ⚠️ An effect runs with NO principal (BPM agent → CLI-script worker): what it may do is decided here, by the
|
|
6
|
-
* instance it names — a FINA_JV_SUBMISSION instance standing at DISETUJUI / DITOLAK (submission.js `post` / `reject`)
|
|
7
|
-
* — and the JV is posted as the submitter, never as anyone a script names.
|
|
8
|
-
*/
|
|
9
|
-
import { finaError } from "./errors.js";
|
|
10
|
-
import { finaCloudConfig } from "./config.js";
|
|
11
|
-
import { createIdentity } from "./identity.js";
|
|
12
|
-
import { createSettingsStore } from "./settings.js";
|
|
13
|
-
import { createFeatures } from "./features.js";
|
|
14
|
-
import { createDocumentWriter } from "./documentWrite.js";
|
|
15
|
-
import { createSubmissionService } from "./submission.js";
|
|
16
|
-
import { createSubmissionRepo } from "./submissionRepo.js";
|
|
17
|
-
import { createApprovalBpm } from "./approval.js";
|
|
18
|
-
import { createAttachmentStore } from "../attachments/store.js";
|
|
19
|
-
|
|
20
|
-
export const NOTICE = "FINA_JV_SUBMISSION";
|
|
21
|
-
|
|
22
|
-
export const submissionServiceOf = (deps, config) => {
|
|
23
|
-
const identity = createIdentity(deps);
|
|
24
|
-
const settings = createSettingsStore(deps);
|
|
25
|
-
const features = createFeatures(deps);
|
|
26
|
-
const attachments = createAttachmentStore({ exec: deps.exec });
|
|
27
|
-
return createSubmissionService({
|
|
28
|
-
client: deps.client,
|
|
29
|
-
identity,
|
|
30
|
-
settings,
|
|
31
|
-
features,
|
|
32
|
-
repo: createSubmissionRepo({ exec: deps.exec }),
|
|
33
|
-
bpm: deps.approvalBpm ?? createApprovalBpm({ exec: deps.exec, dbIndex: config?.tenantDbIndex }),
|
|
34
|
-
attachments,
|
|
35
|
-
writer: createDocumentWriter({ client: deps.client, identity, settings, features, attachments }),
|
|
36
|
-
});
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/*
|
|
40
|
-
* `lib.fina` — `apiConfig` is the hub's (argv: --finaCloudDbindex, --dbindex, the FINA API's host). An effect's data
|
|
41
|
-
* arrives wrapped `{ body, headers }`. FINA Cloud off → NOT_CONFIGURED, THROWN: the effect then shows failed in the
|
|
42
|
-
* BPM module and can be retried once it is on. Every other outcome is an answer (submission.js never throws).
|
|
43
|
-
*/
|
|
44
|
-
export const finaScriptLib = (apiConfig, { emitUsers, serviceOf } = {}) => {
|
|
45
|
-
const config = finaCloudConfig(apiConfig);
|
|
46
|
-
const service = async () => {
|
|
47
|
-
if (!config.enabled) throw finaError("NOT_CONFIGURED", "FINA Cloud is not switched on for this company (the hub runs without --finaCloudDbindex).");
|
|
48
|
-
if (serviceOf) return serviceOf(config);
|
|
49
|
-
const { createLiveFinaDeps } = await import("./live.js");
|
|
50
|
-
return submissionServiceOf(createLiveFinaDeps(config), config);
|
|
51
|
-
};
|
|
52
|
-
const run = (action) => async (scriptData) => {
|
|
53
|
-
const body = scriptData?.body && typeof scriptData.body === "object" ? scriptData.body : (scriptData ?? {});
|
|
54
|
-
const { notify, ...answer } = await (await service())[action]({ instanceId: body.instanceId });
|
|
55
|
-
if (notify?.userIds?.length > 0 && typeof emitUsers === "function") emitUsers(notify.userIds, NOTICE, notify.data);
|
|
56
|
-
return answer;
|
|
57
|
-
};
|
|
58
|
-
return { approval: { post: run("post"), reject: run("reject") } };
|
|
59
|
-
};
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 M6 — a FINA document's attachments (F-34–F-37, L-11): the FINA rule in front of the platform store
|
|
3
|
-
* (engine/attachments/store.js). The files live in Biz-A (SYS$ATTACHMENT), never in FINA, so FINA desktop cannot see
|
|
4
|
-
* them (F-36).
|
|
5
|
-
*
|
|
6
|
-
* - Listing and reading: any mapped FINA user (D4: view), of a document FINA has.
|
|
7
|
-
* - Adding, removing and copying onto a document: FINA's create OR edit right (Agung 2026-10-12), of a document FINA
|
|
8
|
-
* has, whose date is open: neither Biz-A-locked (C-14) nor in a closed FINA period (C-15), as for the document itself.
|
|
9
|
-
* - The limits are the console's (FINA$SETTING `<document>.attachments`, F-35).
|
|
10
|
-
*
|
|
11
|
-
* Only a document whose descriptor says `attachments: true` has any.
|
|
12
|
-
*
|
|
13
|
-
* Doc 8 M7 — a JV SUBMISSION has its own (`{ submission: 'jv', id }` → owner `fina.jvsub`): read by any mapped user,
|
|
14
|
-
* changed by the create or edit right only while it waits (MENUNGGU) on an open date, the JV's limits. On posting they
|
|
15
|
-
* are copied to the new JV by the server (submission.js). A copy (Salin) may come from either kind: `fromOwner` names
|
|
16
|
-
* the source's owner props when it is not the target's kind (a FINA JV's files onto a new submission).
|
|
17
|
-
*/
|
|
18
|
-
import { finaError } from "./errors.js";
|
|
19
|
-
import { requireRight } from "./identity.js";
|
|
20
|
-
import { documentOf, documentId } from "./documents/index.js";
|
|
21
|
-
import { readMaster } from "./document.js";
|
|
22
|
-
import { isDateLocked, toDateOnly } from "./lock.js";
|
|
23
|
-
import { checkPeriods } from "./period.js";
|
|
24
|
-
|
|
25
|
-
export const ownerOf = (name, id) => ({ type: `fina.${name}`, id: String(id) });
|
|
26
|
-
export const submissionOwnerOf = (id) => ({ type: "fina.jvsub", id: String(id) });
|
|
27
|
-
|
|
28
|
-
export const createFinaAttachments = ({ client, identity, settings, store, submissions = null }) => {
|
|
29
|
-
const documentWithAttachments = (name) => {
|
|
30
|
-
const document = documentOf(name);
|
|
31
|
-
if (!document.attachments) throw finaError("INVALID", `A ${document.label} has no attachments in Biz-A.`);
|
|
32
|
-
return document;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const existing = async (document, id) => {
|
|
36
|
-
const master = await readMaster(client, document, id);
|
|
37
|
-
if (!master) throw finaError("DOCUMENT_NOT_FOUND", `${document.label} ${id} was not found.`);
|
|
38
|
-
return master;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/* Any mapped FINA user, of a document FINA has. */
|
|
42
|
-
const readable = async (principal, name, rawId) => {
|
|
43
|
-
const document = documentWithAttachments(name);
|
|
44
|
-
const id = documentId(rawId);
|
|
45
|
-
requireRight(await identity.resolve(principal), document.rights, null);
|
|
46
|
-
await existing(document, id);
|
|
47
|
-
return { document, owner: ownerOf(document.name, id) };
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/* The create OR edit right; with `open`, also a document FINA has, on an open date, and the console limits. */
|
|
51
|
-
const changeable = async (principal, name, rawId, { open = true } = {}) => {
|
|
52
|
-
const document = documentWithAttachments(name);
|
|
53
|
-
const id = documentId(rawId);
|
|
54
|
-
const who = requireRight(await identity.resolve(principal), document.rights, null);
|
|
55
|
-
const rights = who.rights?.[document.rights] ?? {};
|
|
56
|
-
if (rights.create !== true && rights.edit !== true) {
|
|
57
|
-
throw finaError("FORBIDDEN", `Your FINA user ${who.finaUser?.name} may not change the attachments of ${document.label} records.`);
|
|
58
|
-
}
|
|
59
|
-
const owner = ownerOf(document.name, id);
|
|
60
|
-
if (!open) return { document, owner };
|
|
61
|
-
const master = await existing(document, id);
|
|
62
|
-
const config = await settings.read();
|
|
63
|
-
const date = toDateOnly(master.transdate);
|
|
64
|
-
const upTo = config?.lock?.upTo ?? null;
|
|
65
|
-
if (isDateLocked(date, upTo)) {
|
|
66
|
-
throw finaError("DATE_LOCKED", `${date} is on or before the Biz-A lock date ${toDateOnly(upTo)}: its attachments cannot be changed from Biz-A.`);
|
|
67
|
-
}
|
|
68
|
-
await checkPeriods(client, [date]);
|
|
69
|
-
return { document, owner, limits: config?.[document.name]?.attachments ?? null };
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
/* Doc 8 M7 — a JV submission (FINA$JV_SUBMISSION): it must exist; changed only while it waits. */
|
|
73
|
-
const submissionOf = async (name, rawId) => {
|
|
74
|
-
const document = documentWithAttachments(name);
|
|
75
|
-
if (document.name !== "jv" || !submissions) throw finaError("INVALID", `A ${document.label} has no submissions.`);
|
|
76
|
-
const id = documentId(rawId);
|
|
77
|
-
const row = await submissions.read(id);
|
|
78
|
-
if (!row) throw finaError("DOCUMENT_NOT_FOUND", `JV submission ${id} was not found.`);
|
|
79
|
-
return { document, id, row };
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const submissionReadable = async (principal, name, rawId) => {
|
|
83
|
-
requireRight(await identity.resolve(principal), "jv", null);
|
|
84
|
-
const { document, id } = await submissionOf(name, rawId);
|
|
85
|
-
return { document, owner: submissionOwnerOf(id) };
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const submissionChangeable = async (principal, name, rawId, { open = true } = {}) => {
|
|
89
|
-
const who = requireRight(await identity.resolve(principal), "jv", null);
|
|
90
|
-
const { document, id, row } = await submissionOf(name, rawId);
|
|
91
|
-
const rights = who.rights?.[document.rights] ?? {};
|
|
92
|
-
if (rights.create !== true && rights.edit !== true) {
|
|
93
|
-
throw finaError("FORBIDDEN", `Your FINA user ${who.finaUser?.name} may not change the attachments of ${document.label} submissions.`);
|
|
94
|
-
}
|
|
95
|
-
const owner = submissionOwnerOf(id);
|
|
96
|
-
if (!open) return { document, owner };
|
|
97
|
-
if (row.status !== "MENUNGGU") {
|
|
98
|
-
const name = row.number ?? `JV submission ${id}`;
|
|
99
|
-
throw finaError("DOCUMENT_RULE", `${name} is ${String(row.status).toLowerCase()}: its attachments are read only.`);
|
|
100
|
-
}
|
|
101
|
-
const config = await settings.read();
|
|
102
|
-
const date = toDateOnly(row.transdate);
|
|
103
|
-
const upTo = config?.lock?.upTo ?? null;
|
|
104
|
-
if (isDateLocked(date, upTo)) {
|
|
105
|
-
throw finaError("DATE_LOCKED", `${date} is on or before the Biz-A lock date ${toDateOnly(upTo)}: its attachments cannot be changed from Biz-A.`);
|
|
106
|
-
}
|
|
107
|
-
await checkPeriods(client, [date]);
|
|
108
|
-
return { document, owner, limits: config?.[document.name]?.attachments ?? null };
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
/* The owner props a request names: `{ submission }` or `{ document }`. */
|
|
112
|
-
const kindOf = (props) => {
|
|
113
|
-
if (!props || typeof props !== "object" || Array.isArray(props)) throw finaError("INVALID", "An attachment owner is { document } or { submission }.");
|
|
114
|
-
const isSubmission = props.submission !== undefined && props.submission !== null && props.submission !== "";
|
|
115
|
-
return isSubmission ? { name: props.submission, readable: submissionReadable, changeable: submissionChangeable } : { name: props.document, readable, changeable };
|
|
116
|
-
};
|
|
117
|
-
const toRead = (principal, props, id) => {
|
|
118
|
-
const kind = kindOf(props);
|
|
119
|
-
return kind.readable(principal, kind.name, id);
|
|
120
|
-
};
|
|
121
|
-
const toChange = (principal, props, id, options) => {
|
|
122
|
-
const kind = kindOf(props);
|
|
123
|
-
return kind.changeable(principal, kind.name, id, options);
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
list: async (principal, request = {}) => {
|
|
128
|
-
const { owner } = await toRead(principal, request, request.id);
|
|
129
|
-
return store.list({ owner });
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
read: async (principal, request = {}) => {
|
|
133
|
-
const { owner } = await toRead(principal, request, request.id);
|
|
134
|
-
return store.read({ owner, id: request.attachmentId, offset: request.offset });
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
begin: async (principal, request = {}) => {
|
|
138
|
-
const { owner, limits } = await toChange(principal, request, request.id);
|
|
139
|
-
return store.begin({ owner, fileName: request.fileName, size: request.size, userId: principal?.userId }, limits);
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
/* A chunk needs only the right: its upload was opened by `begin`, which checked the record and its date. */
|
|
143
|
-
append: async (principal, request = {}) => {
|
|
144
|
-
const { owner } = await toChange(principal, request, request.id, { open: false });
|
|
145
|
-
return store.append({ owner, id: request.uploadId, chunk: request.chunk });
|
|
146
|
-
},
|
|
147
|
-
|
|
148
|
-
finish: async (principal, request = {}) => {
|
|
149
|
-
const { owner } = await toChange(principal, request, request.id);
|
|
150
|
-
return store.finish({ owner, id: request.uploadId });
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
remove: async (principal, request = {}) => {
|
|
154
|
-
const { owner } = await toChange(principal, request, request.id);
|
|
155
|
-
return store.remove({ owner, ids: request.ids });
|
|
156
|
-
},
|
|
157
|
-
|
|
158
|
-
/* Salin: `from` of `fromOwner` (the target's own kind when absent) onto `to`. */
|
|
159
|
-
copy: async (principal, request = {}) => {
|
|
160
|
-
const source = await toRead(principal, request.fromOwner ?? request, request.from);
|
|
161
|
-
const target = await toChange(principal, request, request.to);
|
|
162
|
-
return store.copy({ from: source.owner, to: target.owner, ids: request.ids, userId: principal?.userId }, target.limits);
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
};
|