@versionone/agility-sdk 0.0.0-stage → 1.0.0-alpha.2
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 +103 -2
- package/dist/index.cjs +1681 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +5144 -0
- package/dist/index.d.ts +5144 -0
- package/dist/index.js +1618 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1681 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ASSET_STATE_CODES: () => ASSET_STATE_CODES,
|
|
24
|
+
AgilityApiClient: () => AgilityApiClient,
|
|
25
|
+
ApiError: () => ApiError,
|
|
26
|
+
AttachmentUploadError: () => AttachmentUploadError,
|
|
27
|
+
BatchRecordError: () => BatchRecordError,
|
|
28
|
+
BatchSendError: () => BatchSendError,
|
|
29
|
+
CommandFailureError: () => CommandFailureError,
|
|
30
|
+
INLINE_LIMIT: () => INLINE_LIMIT,
|
|
31
|
+
INVERSE: () => INVERSE,
|
|
32
|
+
NUMBER_ROOTS: () => NUMBER_ROOTS,
|
|
33
|
+
RESOLVE_CHUNK: () => CHUNK3,
|
|
34
|
+
Schema: () => Schema,
|
|
35
|
+
TypeCache: () => TypeCache,
|
|
36
|
+
UNGUARDABLE_TYPES: () => UNGUARDABLE_TYPES,
|
|
37
|
+
UnguardableError: () => UnguardableError,
|
|
38
|
+
assetApi: () => assetApi_exports,
|
|
39
|
+
assetStateOf: () => assetStateOf,
|
|
40
|
+
attachment: () => attachment_exports,
|
|
41
|
+
createFetchTransport: () => createFetchTransport,
|
|
42
|
+
describeBatch: () => describe,
|
|
43
|
+
guard: () => guard,
|
|
44
|
+
isAssetNumber: () => isAssetNumber,
|
|
45
|
+
isOidToken: () => isOidToken,
|
|
46
|
+
loc2: () => loc2_exports,
|
|
47
|
+
meta: () => meta_exports,
|
|
48
|
+
momentless: () => momentless,
|
|
49
|
+
namesOf: () => namesOf,
|
|
50
|
+
parseOid: () => parseOid,
|
|
51
|
+
queryV1: () => queryV1_exports,
|
|
52
|
+
resolveNumbers: () => resolveNumbers,
|
|
53
|
+
resolveRef: () => resolveRef,
|
|
54
|
+
rest1: () => rest1_exports,
|
|
55
|
+
reverseOf: () => reverseOf,
|
|
56
|
+
reversible: () => reversible,
|
|
57
|
+
runBatch: () => runBatch,
|
|
58
|
+
select: () => select_exports,
|
|
59
|
+
sendGuarded: () => sendGuarded,
|
|
60
|
+
snapshot: () => snapshot,
|
|
61
|
+
stampsOf: () => stampsOf,
|
|
62
|
+
toOid: () => toOid,
|
|
63
|
+
touchedBy: () => touchedBy,
|
|
64
|
+
typeOfOid: () => typeOfOid,
|
|
65
|
+
unguardable: () => unguardable
|
|
66
|
+
});
|
|
67
|
+
module.exports = __toCommonJS(index_exports);
|
|
68
|
+
|
|
69
|
+
// src/core/errors.ts
|
|
70
|
+
var ApiError = class extends Error {
|
|
71
|
+
status;
|
|
72
|
+
code;
|
|
73
|
+
details;
|
|
74
|
+
constructor(message, status, code, details) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = "ApiError";
|
|
77
|
+
this.status = status;
|
|
78
|
+
this.code = code;
|
|
79
|
+
this.details = details;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/core/fetchTransport.ts
|
|
84
|
+
function abortedError(signal) {
|
|
85
|
+
return new ApiError("Request aborted", 0, "ABORTED", { cause: signal.reason });
|
|
86
|
+
}
|
|
87
|
+
function buildUrl(baseUrl, path, query3) {
|
|
88
|
+
const normalizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
89
|
+
const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
|
|
90
|
+
const url = new URL(normalizedPath, normalizedBase);
|
|
91
|
+
if (query3) {
|
|
92
|
+
for (const [key, value] of Object.entries(query3)) {
|
|
93
|
+
if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return url.toString();
|
|
97
|
+
}
|
|
98
|
+
function encodeBody(body, contentType) {
|
|
99
|
+
if (body === void 0) return {};
|
|
100
|
+
if (typeof body === "string") return { body, contentType: contentType ?? "text/plain" };
|
|
101
|
+
if (body instanceof Uint8Array || body instanceof ArrayBuffer) {
|
|
102
|
+
return { body, contentType: contentType ?? "application/octet-stream" };
|
|
103
|
+
}
|
|
104
|
+
if (typeof FormData !== "undefined" && body instanceof FormData) return { body };
|
|
105
|
+
if (typeof Blob !== "undefined" && body instanceof Blob) return { body, contentType: contentType ?? (body.type || "application/octet-stream") };
|
|
106
|
+
return { body: JSON.stringify(body), contentType: contentType ?? "application/json" };
|
|
107
|
+
}
|
|
108
|
+
async function readBody(response, responseType) {
|
|
109
|
+
if (responseType === "bytes") return new Uint8Array(await response.arrayBuffer());
|
|
110
|
+
if (responseType === "text") return response.text();
|
|
111
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
112
|
+
if (responseType === "json" || contentType.includes("json")) {
|
|
113
|
+
const text = await response.text();
|
|
114
|
+
if (!text) return void 0;
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(text);
|
|
117
|
+
} catch {
|
|
118
|
+
if (responseType === "json") throw new ApiError("Response is not JSON", response.status, "BAD_JSON", { body: text.slice(0, 500) });
|
|
119
|
+
return text;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return response.text();
|
|
123
|
+
}
|
|
124
|
+
function messageOf(data, status) {
|
|
125
|
+
if (data && typeof data === "object") {
|
|
126
|
+
const d = data;
|
|
127
|
+
const first = Array.isArray(d.exceptions) && d.exceptions[0] && typeof d.exceptions[0] === "object" ? d.exceptions[0].message : void 0;
|
|
128
|
+
const listed = Array.isArray(d.Exceptions) ? d.Exceptions.filter((x) => typeof x === "string" && !!x).join("; ") : void 0;
|
|
129
|
+
for (const candidate of [d.error, first, listed, d.message]) {
|
|
130
|
+
if (typeof candidate === "string" && candidate) return candidate;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (typeof data === "string" && data.trim()) return data.trim().slice(0, 200);
|
|
134
|
+
return `HTTP ${status}`;
|
|
135
|
+
}
|
|
136
|
+
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
137
|
+
function createFetchTransport(config) {
|
|
138
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
139
|
+
const timeoutMs = config.timeoutMs ?? 6e4;
|
|
140
|
+
const retries = Math.max(0, config.retries ?? 0);
|
|
141
|
+
const authorization = config.authorization ?? (config.accessToken ? `Bearer ${config.accessToken}` : void 0);
|
|
142
|
+
const unauthorizedStatuses = config.unauthorizedStatuses ?? [401];
|
|
143
|
+
return {
|
|
144
|
+
async request(options) {
|
|
145
|
+
const url = buildUrl(config.baseUrl, options.path, options.query);
|
|
146
|
+
const { body, contentType } = encodeBody(options.body, options.contentType);
|
|
147
|
+
const headers = { accept: "application/json", ...config.defaultHeaders, ...options.headers };
|
|
148
|
+
if (contentType) headers["content-type"] = contentType;
|
|
149
|
+
if (authorization) headers.authorization = authorization;
|
|
150
|
+
const responseType = options.responseType ?? "auto";
|
|
151
|
+
const idempotent = options.idempotent ?? options.method === "GET";
|
|
152
|
+
const attempts = idempotent ? retries + 1 : 1;
|
|
153
|
+
const callerSignal = options.signal;
|
|
154
|
+
let lastError;
|
|
155
|
+
let reauthorized = false;
|
|
156
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
157
|
+
if (callerSignal?.aborted) throw abortedError(callerSignal);
|
|
158
|
+
const controller = new AbortController();
|
|
159
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
160
|
+
const onAbort = () => controller.abort();
|
|
161
|
+
callerSignal?.addEventListener("abort", onAbort, { once: true });
|
|
162
|
+
const settle = () => {
|
|
163
|
+
clearTimeout(timeoutId);
|
|
164
|
+
callerSignal?.removeEventListener("abort", onAbort);
|
|
165
|
+
};
|
|
166
|
+
let response;
|
|
167
|
+
try {
|
|
168
|
+
const init = { method: options.method, headers, body, signal: controller.signal };
|
|
169
|
+
if (config.credentials) init.credentials = config.credentials;
|
|
170
|
+
response = await fetchImpl(url, init);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
settle();
|
|
173
|
+
if (callerSignal?.aborted) throw abortedError(callerSignal);
|
|
174
|
+
lastError = error instanceof Error && error.name === "AbortError" ? new ApiError(`Timed out after ${timeoutMs} ms`, 0, "TIMEOUT", { cause: error }) : new ApiError(error instanceof Error ? error.message : String(error), 0, "NETWORK_ERROR", { cause: error });
|
|
175
|
+
if (attempt + 1 < attempts) await delay(150 * (attempt + 1));
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
if (response.ok) {
|
|
180
|
+
options.onResponse?.({ status: response.status, headers: response.headers });
|
|
181
|
+
return await readBody(response, responseType);
|
|
182
|
+
}
|
|
183
|
+
const data = await readBody(response, responseType === "bytes" ? "text" : responseType === "json" ? "auto" : responseType);
|
|
184
|
+
lastError = new ApiError(messageOf(data, response.status), response.status, void 0, data);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (callerSignal?.aborted) throw abortedError(callerSignal);
|
|
187
|
+
throw error;
|
|
188
|
+
} finally {
|
|
189
|
+
settle();
|
|
190
|
+
}
|
|
191
|
+
if (config.onUnauthorized && !reauthorized && unauthorizedStatuses.includes(response.status)) {
|
|
192
|
+
reauthorized = true;
|
|
193
|
+
const decision = await config.onUnauthorized({ error: lastError, request: options, retryable: idempotent });
|
|
194
|
+
if (callerSignal?.aborted) throw abortedError(callerSignal);
|
|
195
|
+
if (decision === "retry" && idempotent) {
|
|
196
|
+
attempt -= 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
if (response.status >= 500 && attempt + 1 < attempts) {
|
|
202
|
+
await delay(150 * (attempt + 1));
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
throw lastError instanceof ApiError ? lastError : new ApiError("Unknown transport error", 0, "UNKNOWN_ERROR", { cause: lastError });
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/types/oid.ts
|
|
213
|
+
var OID_TOKEN = /^([A-Za-z][A-Za-z0-9_]*):(\d+)(?::(\d+))?$/;
|
|
214
|
+
function isOidToken(value) {
|
|
215
|
+
return typeof value === "string" && OID_TOKEN.test(value);
|
|
216
|
+
}
|
|
217
|
+
function parseOid(token) {
|
|
218
|
+
const m = OID_TOKEN.exec(token);
|
|
219
|
+
if (!m) throw new Error(`"${token}" is not an OID token (Story:1234)`);
|
|
220
|
+
return { type: m[1], id: Number(m[2]), moment: m[3] === void 0 ? null : Number(m[3]) };
|
|
221
|
+
}
|
|
222
|
+
function momentless(token) {
|
|
223
|
+
const m = OID_TOKEN.exec(token);
|
|
224
|
+
return m ? `${m[1]}:${m[2]}` : token;
|
|
225
|
+
}
|
|
226
|
+
function typeOfOid(token) {
|
|
227
|
+
return parseOid(token).type;
|
|
228
|
+
}
|
|
229
|
+
function toOid(type, id, moment) {
|
|
230
|
+
return moment == null ? `${type}:${id}` : `${type}:${id}:${moment}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/wire/assetApi.ts
|
|
234
|
+
var assetApi_exports = {};
|
|
235
|
+
__export(assetApi_exports, {
|
|
236
|
+
CommandFailureError: () => CommandFailureError,
|
|
237
|
+
emptyOutcome: () => emptyOutcome,
|
|
238
|
+
failuresOf: () => failuresOf,
|
|
239
|
+
fromWireRow: () => fromWireRow,
|
|
240
|
+
mergeOutcome: () => mergeOutcome,
|
|
241
|
+
momentless: () => momentless,
|
|
242
|
+
outcomeOf: () => outcomeOf,
|
|
243
|
+
preview: () => preview,
|
|
244
|
+
queries: () => queries,
|
|
245
|
+
query: () => query,
|
|
246
|
+
queryPage: () => queryPage,
|
|
247
|
+
send: () => send,
|
|
248
|
+
sendYaml: () => sendYaml,
|
|
249
|
+
toWire: () => toWire,
|
|
250
|
+
toWireValue: () => toWireValue
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// src/wire/select.ts
|
|
254
|
+
var select_exports = {};
|
|
255
|
+
__export(select_exports, {
|
|
256
|
+
attributeOf: () => attributeOf,
|
|
257
|
+
fromTypeOf: () => fromTypeOf,
|
|
258
|
+
resolvePath: () => resolvePath,
|
|
259
|
+
segmentsOf: () => segmentsOf,
|
|
260
|
+
typesNeeded: () => typesNeeded
|
|
261
|
+
});
|
|
262
|
+
function segmentsOf(path) {
|
|
263
|
+
const parts = [];
|
|
264
|
+
let depth = 0, quote = null, current = "";
|
|
265
|
+
for (const ch of path) {
|
|
266
|
+
if (quote) {
|
|
267
|
+
current += ch;
|
|
268
|
+
if (ch === quote) quote = null;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (ch === "'" || ch === '"') {
|
|
272
|
+
quote = ch;
|
|
273
|
+
current += ch;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (ch === "[") depth += 1;
|
|
277
|
+
if (ch === "]") depth -= 1;
|
|
278
|
+
if (ch === "." && depth === 0) {
|
|
279
|
+
parts.push(current);
|
|
280
|
+
current = "";
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
current += ch;
|
|
284
|
+
}
|
|
285
|
+
parts.push(current);
|
|
286
|
+
return parts.map((raw) => {
|
|
287
|
+
if (raw.startsWith("@")) return { name: raw, downcast: null, aggregate: raw.slice(1) };
|
|
288
|
+
const bare = raw.replace(/\[[\s\S]*\]$/, "");
|
|
289
|
+
const colon = bare.indexOf(":");
|
|
290
|
+
return colon < 0 ? { name: bare, downcast: null, aggregate: null } : { name: bare.slice(0, colon), downcast: bare.slice(colon + 1) || null, aggregate: null };
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
function attributeOf(lookup, type, name) {
|
|
294
|
+
const t = lookup.assetType(type);
|
|
295
|
+
if (t === void 0) return void 0;
|
|
296
|
+
if (t === null) return null;
|
|
297
|
+
return t.Attributes[`${type}.${name}`] ?? Object.values(t.Attributes).find((a) => a.Name === name) ?? null;
|
|
298
|
+
}
|
|
299
|
+
function resolvePath(lookup, type, path) {
|
|
300
|
+
const segments = segmentsOf(path);
|
|
301
|
+
let current = type, multi = false, last = null;
|
|
302
|
+
for (const [i, seg] of segments.entries()) {
|
|
303
|
+
if (seg.aggregate) {
|
|
304
|
+
return i === segments.length - 1 ? { kind: "resolved", definition: null, valueType: null, multi: false, aggregate: seg.aggregate } : { kind: "unknown", at: path };
|
|
305
|
+
}
|
|
306
|
+
const def = attributeOf(lookup, current, seg.name);
|
|
307
|
+
if (def === void 0) return { kind: "needs", type: current };
|
|
308
|
+
if (def === null) return { kind: "unknown", at: `${current}.${seg.name}` };
|
|
309
|
+
if (def.AttributeType !== "Relation") {
|
|
310
|
+
return i === segments.length - 1 ? { kind: "resolved", definition: def, valueType: current, multi, aggregate: null } : { kind: "unknown", at: path };
|
|
311
|
+
}
|
|
312
|
+
multi ||= def.IsMultivalue;
|
|
313
|
+
const next = seg.downcast ?? def.RelatedAsset;
|
|
314
|
+
if (!next) return { kind: "unknown", at: `${current}.${seg.name}` };
|
|
315
|
+
current = next;
|
|
316
|
+
last = def;
|
|
317
|
+
}
|
|
318
|
+
return { kind: "resolved", definition: last, valueType: current, multi, aggregate: null };
|
|
319
|
+
}
|
|
320
|
+
function fromTypeOf(from, batch = []) {
|
|
321
|
+
const fromOid = isOidToken(from) ? typeOfOid(from) : null;
|
|
322
|
+
if (fromOid) return fromOid;
|
|
323
|
+
if (from.startsWith("@")) {
|
|
324
|
+
for (const command of batch) {
|
|
325
|
+
const body = command?.[from];
|
|
326
|
+
const assetType = body && typeof body === "object" ? body.AssetType : void 0;
|
|
327
|
+
if (typeof assetType === "string") return assetType;
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
return from;
|
|
332
|
+
}
|
|
333
|
+
function typesNeeded(lookup, query3, type) {
|
|
334
|
+
const needed = /* @__PURE__ */ new Set();
|
|
335
|
+
if (lookup.assetType(type) === void 0) {
|
|
336
|
+
needed.add(type);
|
|
337
|
+
return needed;
|
|
338
|
+
}
|
|
339
|
+
for (const entry of query3.select ?? []) {
|
|
340
|
+
const path = typeof entry === "string" ? entry : entry.from;
|
|
341
|
+
const r = resolvePath(lookup, type, path);
|
|
342
|
+
if (r.kind === "needs") needed.add(r.type);
|
|
343
|
+
else if (r.kind === "resolved" && typeof entry !== "string" && r.valueType) for (const t of typesNeeded(lookup, entry, r.valueType)) needed.add(t);
|
|
344
|
+
}
|
|
345
|
+
return needed;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/wire/assetApi.ts
|
|
349
|
+
var isRef = (v) => !!v && typeof v === "object" && !Array.isArray(v) && "_oid" in v && typeof v._oid === "string";
|
|
350
|
+
function toWireValue(value) {
|
|
351
|
+
if (isRef(value)) return momentless(value._oid);
|
|
352
|
+
if (Array.isArray(value)) return value.map(toWireValue);
|
|
353
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, toWireValue(v)]));
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
var toWire = (commands) => commands.map((c) => toWireValue(c));
|
|
357
|
+
function fromWireRow(row, context) {
|
|
358
|
+
const out = {};
|
|
359
|
+
const grouped = Array.isArray(row._children);
|
|
360
|
+
for (const [k, v] of Object.entries(row)) {
|
|
361
|
+
if (grouped && k === "_children") out[k] = v.map((child) => child && typeof child === "object" ? fromWireRow(child, context) : child);
|
|
362
|
+
else if (grouped && k === "_oid" && v === "NULL") out[k] = null;
|
|
363
|
+
else if (k === "_oid" && typeof v === "string") out[k] = momentless(v);
|
|
364
|
+
else out[k] = context ? translate(v, resolvePath(context.lookup, context.type, k), context.lookup) : fromWireValue(v);
|
|
365
|
+
}
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
var isNullRef = (value) => isRef(value) && value._oid === "NULL";
|
|
369
|
+
function fromWireValue(value) {
|
|
370
|
+
if (isNullRef(value)) return null;
|
|
371
|
+
if (isRef(value)) return { ...value, _oid: momentless(value._oid) };
|
|
372
|
+
if (Array.isArray(value)) return value.map(fromWireValue);
|
|
373
|
+
return value;
|
|
374
|
+
}
|
|
375
|
+
function translate(value, resolution, lookup) {
|
|
376
|
+
if (resolution.kind !== "resolved") return fromWireValue(value);
|
|
377
|
+
if (resolution.aggregate) return toNumber(value);
|
|
378
|
+
const definition = resolution.definition;
|
|
379
|
+
if (definition.AttributeType === "Relation") {
|
|
380
|
+
const context = resolution.valueType ? { lookup, type: resolution.valueType } : void 0;
|
|
381
|
+
const one2 = (x) => isNullRef(x) ? null : isRef(x) ? fromWireRow(x, context) : fromWireValue(x);
|
|
382
|
+
if (resolution.multi) return (Array.isArray(value) ? value : value == null ? [] : [value]).filter((x) => !isNullRef(x)).map(one2);
|
|
383
|
+
if (Array.isArray(value)) return value.length ? one2(value[0]) : null;
|
|
384
|
+
return value == null ? null : one2(value);
|
|
385
|
+
}
|
|
386
|
+
const one = (x) => scalar(x, definition.AttributeType);
|
|
387
|
+
return resolution.multi && Array.isArray(value) ? value.map(one) : one(value);
|
|
388
|
+
}
|
|
389
|
+
function scalar(value, type) {
|
|
390
|
+
if (value == null) return null;
|
|
391
|
+
switch (type) {
|
|
392
|
+
case "Numeric":
|
|
393
|
+
case "LongInt":
|
|
394
|
+
case "Rank":
|
|
395
|
+
return toNumber(value);
|
|
396
|
+
case "Boolean":
|
|
397
|
+
return value === true || value === "True" || value === "true" ? true : value === false || value === "False" || value === "false" ? false : value;
|
|
398
|
+
default:
|
|
399
|
+
return value;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function toNumber(value) {
|
|
403
|
+
if (value == null) return null;
|
|
404
|
+
if (typeof value === "number") return value;
|
|
405
|
+
const n = typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
|
|
406
|
+
return Number.isNaN(n) ? value : n;
|
|
407
|
+
}
|
|
408
|
+
function failuresOf(response) {
|
|
409
|
+
return (response.commandFailures?.commands ?? []).map(({ error, ...echoed }) => ({
|
|
410
|
+
index: typeof error === "object" && error && typeof error.sourceCommandIndex === "number" ? error.sourceCommandIndex : null,
|
|
411
|
+
message: typeof error === "string" ? error : error?.message || "command failed",
|
|
412
|
+
echoed
|
|
413
|
+
}));
|
|
414
|
+
}
|
|
415
|
+
function outcomeOf(response) {
|
|
416
|
+
return {
|
|
417
|
+
created: (response.assetsCreated?.oidTokens ?? []).map(momentless),
|
|
418
|
+
modified: (response.assetsModified?.oidTokens ?? []).map(momentless),
|
|
419
|
+
operated: (response.assetsOperatedOn?.oidTokens ?? []).map(momentless),
|
|
420
|
+
failures: failuresOf(response),
|
|
421
|
+
queryResults: response.queryResult?.results ?? [],
|
|
422
|
+
durationSeconds: typeof response.durationSeconds === "number" ? response.durationSeconds : null
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
var emptyOutcome = () => ({ created: [], modified: [], operated: [], failures: [], queryResults: [], durationSeconds: null });
|
|
426
|
+
function mergeOutcome(into, done, offset) {
|
|
427
|
+
into.created.push(...done.created);
|
|
428
|
+
into.modified.push(...done.modified);
|
|
429
|
+
into.operated.push(...done.operated);
|
|
430
|
+
into.failures.push(...done.failures.map((f) => ({ ...f, index: f.index === null ? null : f.index + offset })));
|
|
431
|
+
into.queryResults.push(...done.queryResults);
|
|
432
|
+
if (done.durationSeconds !== null) into.durationSeconds = (into.durationSeconds ?? 0) + done.durationSeconds;
|
|
433
|
+
return into;
|
|
434
|
+
}
|
|
435
|
+
var CommandFailureError = class extends ApiError {
|
|
436
|
+
failures;
|
|
437
|
+
response;
|
|
438
|
+
constructor(response, failures = failuresOf(response)) {
|
|
439
|
+
super(failures.map((f) => f.message).join("; ") || "command failed", 200, "CommandFailure", failures);
|
|
440
|
+
this.name = "CommandFailureError";
|
|
441
|
+
this.failures = failures;
|
|
442
|
+
this.response = response;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
var bodyOf = (c) => {
|
|
446
|
+
const keys = Object.keys(c);
|
|
447
|
+
return keys.length === 1 && keys[0].startsWith("@") ? c[keys[0]] : c;
|
|
448
|
+
};
|
|
449
|
+
var isQuery = (body) => !!body && typeof body === "object" && typeof body.from === "string" && !("update" in body) && !("execute" in body) && !("AssetType" in body);
|
|
450
|
+
function queriesIn(commands) {
|
|
451
|
+
return commands.flatMap((c, index) => {
|
|
452
|
+
const body = bodyOf(c);
|
|
453
|
+
return isQuery(body) ? [{ body, index }] : [];
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
function realign(sent, results, failed) {
|
|
457
|
+
const expected = sent.filter((q) => !failed.has(q.index)).length;
|
|
458
|
+
if (results.length !== expected) return null;
|
|
459
|
+
let next = 0;
|
|
460
|
+
return sent.map((q) => failed.has(q.index) ? null : results[next++]);
|
|
461
|
+
}
|
|
462
|
+
async function send(transport, commands, options = {}) {
|
|
463
|
+
const response = await transport.request({
|
|
464
|
+
method: "POST",
|
|
465
|
+
path: "/api/asset",
|
|
466
|
+
query: options.previewOnly ? { previewOnly: "true" } : void 0,
|
|
467
|
+
body: toWire(commands),
|
|
468
|
+
responseType: "json",
|
|
469
|
+
idempotent: options.idempotent === true
|
|
470
|
+
});
|
|
471
|
+
if (options.previewOnly) return response;
|
|
472
|
+
const queries3 = queriesIn(commands);
|
|
473
|
+
const failures = failuresOf(response);
|
|
474
|
+
const failed = new Set(failures.flatMap((f) => f.index === null ? [] : [f.index]));
|
|
475
|
+
const results = response.queryResult?.results ?? [];
|
|
476
|
+
let aligned = realign(queries3, results, failed);
|
|
477
|
+
if (!aligned) {
|
|
478
|
+
if (!failures.some((f) => f.index === null)) throw new Error(`api/asset returned ${results.length} query results for ${queries3.filter((q) => !failed.has(q.index)).length} queries that did not fail; cannot tell which result belongs to which query`);
|
|
479
|
+
aligned = queries3.map(() => null);
|
|
480
|
+
}
|
|
481
|
+
const contextOf = (i) => {
|
|
482
|
+
const type = options.lookup ? fromTypeOf(queries3[i].body.from, commands) : null;
|
|
483
|
+
return type ? { lookup: options.lookup, type } : void 0;
|
|
484
|
+
};
|
|
485
|
+
const translated = { ...response, queryResult: { ...response.queryResult, results: aligned.map((rows, i) => rows && rows.map((r) => fromWireRow(r, contextOf(i)))) } };
|
|
486
|
+
if (options.throwOnFailure && translated.commandFailures?.count) throw new CommandFailureError(translated);
|
|
487
|
+
return translated;
|
|
488
|
+
}
|
|
489
|
+
async function preview(transport, commands) {
|
|
490
|
+
return transport.request({ method: "POST", path: "/api/asset", query: { previewOnly: "true" }, body: toWire(commands), responseType: "json" });
|
|
491
|
+
}
|
|
492
|
+
async function sendYaml(transport, yaml, options = {}) {
|
|
493
|
+
return transport.request({ method: "POST", path: "/api/asset", query: options.previewOnly ? { previewOnly: "true" } : void 0, body: yaml, contentType: "text/yaml", responseType: "json" });
|
|
494
|
+
}
|
|
495
|
+
async function query(transport, q, options = {}) {
|
|
496
|
+
const [rows] = await queries(transport, [q], options);
|
|
497
|
+
return rows ?? [];
|
|
498
|
+
}
|
|
499
|
+
async function queryPage(transport, q, options = {}) {
|
|
500
|
+
if (!q.page) throw new Error(`query of ${q.from} without a page: every api/asset read must be bounded`);
|
|
501
|
+
const response = await send(transport, [{ ...q, needTotal: true }], { ...options, throwOnFailure: true });
|
|
502
|
+
const rows = response.queryResult.results[0] ?? [];
|
|
503
|
+
const reported = response.queryResult.resultMetaData?.[0]?.total;
|
|
504
|
+
const total = typeof reported === "number" ? reported : null;
|
|
505
|
+
return { rows, total, more: total === null ? rows.length === q.page.size : q.page.start + rows.length < total };
|
|
506
|
+
}
|
|
507
|
+
async function queries(transport, qs, options = {}) {
|
|
508
|
+
for (const q of qs) if (!q.page) throw new Error(`query of ${q.from} without a page: every api/asset read must be bounded`);
|
|
509
|
+
const response = await send(transport, qs, { ...options, throwOnFailure: true });
|
|
510
|
+
return qs.map((_, i) => response.queryResult.results[i] ?? []);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/wire/attachment.ts
|
|
514
|
+
var attachment_exports = {};
|
|
515
|
+
__export(attachment_exports, {
|
|
516
|
+
attachmentUrl: () => attachmentUrl,
|
|
517
|
+
embeddedImageUrl: () => embeddedImageUrl,
|
|
518
|
+
fromBase64: () => fromBase64,
|
|
519
|
+
getBytes: () => getBytes,
|
|
520
|
+
getFile: () => getFile,
|
|
521
|
+
normalizeEmbeddedImageUrls: () => normalizeEmbeddedImageUrls,
|
|
522
|
+
numericId: () => numericId,
|
|
523
|
+
putBytes: () => putBytes,
|
|
524
|
+
toBase64: () => toBase64,
|
|
525
|
+
toUint8Array: () => toUint8Array,
|
|
526
|
+
uploadEmbeddedImage: () => uploadEmbeddedImage
|
|
527
|
+
});
|
|
528
|
+
async function toUint8Array(data) {
|
|
529
|
+
if (data instanceof Uint8Array) return data;
|
|
530
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
531
|
+
return new Uint8Array(await data.arrayBuffer());
|
|
532
|
+
}
|
|
533
|
+
function toBase64(bytes) {
|
|
534
|
+
let binary = "";
|
|
535
|
+
for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
536
|
+
return btoa(binary);
|
|
537
|
+
}
|
|
538
|
+
function fromBase64(text) {
|
|
539
|
+
const binary = atob(text);
|
|
540
|
+
const out = new Uint8Array(binary.length);
|
|
541
|
+
for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);
|
|
542
|
+
return out;
|
|
543
|
+
}
|
|
544
|
+
function numericId(spec) {
|
|
545
|
+
const m = /^(?:[A-Za-z]+:)?(\d+)(?::\d+)?$/.exec(String(spec).trim());
|
|
546
|
+
if (!m) throw new TypeError(`"${spec}" is not an asset id (5678, or an OID token such as Attachment:5678)`);
|
|
547
|
+
return m[1];
|
|
548
|
+
}
|
|
549
|
+
var join = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}/${path}`;
|
|
550
|
+
var attachmentUrl = (baseUrl, id) => join(baseUrl, `attachment.img/${numericId(id)}`);
|
|
551
|
+
var embeddedImageUrl = (baseUrl, id) => join(baseUrl, `embedded.img/${numericId(id)}`);
|
|
552
|
+
function normalizeEmbeddedImageUrls(html, baseUrl) {
|
|
553
|
+
return html.replace(/(\bsrc\s*=\s*)(["'])([^"']*?embedded\.img\/(\d+)[^"']*)\2/gi, (_m, attr, quote, _url, id) => `${attr}${quote}${embeddedImageUrl(baseUrl, id)}${quote}`);
|
|
554
|
+
}
|
|
555
|
+
function named(error, what) {
|
|
556
|
+
if (!(error instanceof ApiError)) return error;
|
|
557
|
+
if (error.status === 404) return new ApiError(`${what}: not found`, 404, "NOT_FOUND", error.details);
|
|
558
|
+
if (error.status === 413) return new ApiError(`${what}: larger than the server's maximum request length`, 413, "TOO_LARGE", error.details);
|
|
559
|
+
return error;
|
|
560
|
+
}
|
|
561
|
+
async function getBytes(transport, id) {
|
|
562
|
+
const n = numericId(id);
|
|
563
|
+
try {
|
|
564
|
+
return await transport.request({ method: "GET", path: `/attachment.img/${n}`, responseType: "bytes" });
|
|
565
|
+
} catch (error) {
|
|
566
|
+
throw named(error, `attachment ${n}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async function getFile(transport, id) {
|
|
570
|
+
const n = numericId(id);
|
|
571
|
+
let contentType = null, filename = null;
|
|
572
|
+
const onResponse = ({ headers }) => {
|
|
573
|
+
contentType = headers.get("content-type");
|
|
574
|
+
const disposition = headers.get("content-disposition") ?? "";
|
|
575
|
+
const encoded = /filename\*=UTF-8''([^;]+)/i.exec(disposition)?.[1];
|
|
576
|
+
filename = encoded ? decodeURIComponent(encoded) : /filename="?([^";]+)"?/i.exec(disposition)?.[1] ?? null;
|
|
577
|
+
};
|
|
578
|
+
try {
|
|
579
|
+
const bytes = await transport.request({ method: "GET", path: `/attachment.img/${n}`, responseType: "bytes", onResponse });
|
|
580
|
+
return { bytes, contentType, filename };
|
|
581
|
+
} catch (error) {
|
|
582
|
+
throw named(error, `attachment ${n}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async function putBytes(transport, id, data, contentType = "application/octet-stream") {
|
|
586
|
+
const n = numericId(id);
|
|
587
|
+
const body = data instanceof Blob ? data : await toUint8Array(data);
|
|
588
|
+
try {
|
|
589
|
+
await transport.request({ method: "POST", path: `/attachment.img/${n}`, body, contentType: data instanceof Blob && data.type ? data.type : contentType, responseType: "text" });
|
|
590
|
+
} catch (error) {
|
|
591
|
+
throw named(error, `attachment ${n}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
async function uploadEmbeddedImage(transport, asset, data, options = {}) {
|
|
595
|
+
const blob = data instanceof Blob ? data : new Blob([await toUint8Array(data)], { type: options.contentType ?? "application/octet-stream" });
|
|
596
|
+
const form = new FormData();
|
|
597
|
+
form.append("image", blob, options.filename ?? "image");
|
|
598
|
+
form.append("oidToken", asset);
|
|
599
|
+
let html;
|
|
600
|
+
try {
|
|
601
|
+
html = await transport.request({ method: "POST", path: "/EmbeddedImage.mvc/Upload", body: form, responseType: "text" });
|
|
602
|
+
} catch (error) {
|
|
603
|
+
throw named(error, `embedded image for ${asset}`);
|
|
604
|
+
}
|
|
605
|
+
const inner = /<textarea[^>]*>([\s\S]*?)<\/textarea>/i.exec(html)?.[1];
|
|
606
|
+
let payload;
|
|
607
|
+
try {
|
|
608
|
+
payload = inner ? JSON.parse(inner.replace(/"/g, '"').replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&")) : void 0;
|
|
609
|
+
} catch {
|
|
610
|
+
}
|
|
611
|
+
if (!payload || typeof payload.Url !== "string") throw new ApiError("EmbeddedImage.mvc/Upload did not answer with an image URL", 200, "BAD_RESPONSE", html.slice(0, 500));
|
|
612
|
+
const id = /embedded\.img\/(\d+)/.exec(payload.Url)?.[1] ?? (typeof payload.Key === "string" ? numericId(payload.Key) : void 0);
|
|
613
|
+
if (!id) throw new ApiError("EmbeddedImage.mvc/Upload answered a URL without an image id", 200, "BAD_RESPONSE", payload);
|
|
614
|
+
return { oid: typeof payload.Key === "string" ? payload.Key : `EmbeddedImage:${id}`, id, url: payload.Url };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/services/attachments.ts
|
|
618
|
+
var INLINE_LIMIT = 1e6;
|
|
619
|
+
var SELECT = ["Name", "Filename", "ContentType", "Description", "CreateDate", "CreatedBy.Name"];
|
|
620
|
+
var infoOf = (r) => {
|
|
621
|
+
const oid = momentless(r._oid);
|
|
622
|
+
return { oid, id: numericId(oid), name: r.Name ?? null, filename: r.Filename ?? null, contentType: r.ContentType ?? null, description: r.Description ?? null, createDate: r.CreateDate ?? null, createdBy: r["CreatedBy.Name"] ?? null };
|
|
623
|
+
};
|
|
624
|
+
async function page(transport, asset, options = {}) {
|
|
625
|
+
const { rows, total, more } = await queryPage(transport, { from: "Attachment", select: SELECT, filter: ["Asset=$asset"], with: { $asset: momentless(asset) }, sort: ["-CreateDate"], page: options.page ?? { size: 50, start: 0 } });
|
|
626
|
+
return { attachments: rows.map((r) => infoOf(r)), total, more };
|
|
627
|
+
}
|
|
628
|
+
async function list(transport, asset, options = {}) {
|
|
629
|
+
const rows = await query(transport, { from: "Attachment", select: SELECT, filter: ["Asset=$asset"], with: { $asset: momentless(asset) }, sort: ["-CreateDate"], page: options.page ?? { size: 50, start: 0 } });
|
|
630
|
+
return rows.map((r) => infoOf(r));
|
|
631
|
+
}
|
|
632
|
+
async function info(transport, id) {
|
|
633
|
+
const [row] = await query(transport, { from: "Attachment", select: SELECT, filter: ["ID=$id"], with: { $id: `Attachment:${numericId(id)}` }, page: { size: 1, start: 0 } });
|
|
634
|
+
return row ? infoOf(row) : null;
|
|
635
|
+
}
|
|
636
|
+
async function get(transport, id) {
|
|
637
|
+
const meta = await info(transport, id);
|
|
638
|
+
if (!meta) throw new ApiError(`attachment ${numericId(id)}: not found`, 404, "NOT_FOUND");
|
|
639
|
+
const file = await getFile(transport, meta.id);
|
|
640
|
+
return { ...meta, bytes: file.bytes, contentType: file.contentType ?? meta.contentType, recordedContentType: meta.contentType };
|
|
641
|
+
}
|
|
642
|
+
var AttachmentUploadError = class extends Error {
|
|
643
|
+
constructor(attachment, cause) {
|
|
644
|
+
super(`${attachment.oid} was created on its asset with no content, and uploading the file failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
645
|
+
this.attachment = attachment;
|
|
646
|
+
this.name = "AttachmentUploadError";
|
|
647
|
+
}
|
|
648
|
+
attachment;
|
|
649
|
+
};
|
|
650
|
+
async function add(transport, asset, data, options) {
|
|
651
|
+
const bytes = await toUint8Array(data);
|
|
652
|
+
const contentType = options.contentType ?? (typeof Blob !== "undefined" && data instanceof Blob && data.type ? data.type : "application/octet-stream");
|
|
653
|
+
const inline = bytes.length <= (options.inlineLimit ?? INLINE_LIMIT);
|
|
654
|
+
const create = { AssetType: "Attachment", Asset: momentless(asset), Name: options.name ?? options.filename, Filename: options.filename, ContentType: contentType, Content: inline ? toBase64(bytes) : "" };
|
|
655
|
+
if (options.description) create.Description = options.description;
|
|
656
|
+
const oid = outcomeOf(await send(transport, [create], { throwOnFailure: true })).created[0];
|
|
657
|
+
const created = { oid, id: numericId(oid), name: String(create.Name), filename: options.filename, contentType, description: options.description ?? null, createDate: null, createdBy: null };
|
|
658
|
+
if (!inline) {
|
|
659
|
+
try {
|
|
660
|
+
await putBytes(transport, created.id, bytes, contentType);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
throw new AttachmentUploadError(created, error);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return created;
|
|
666
|
+
}
|
|
667
|
+
async function put(transport, id, data, contentType) {
|
|
668
|
+
await putBytes(transport, id, data, contentType);
|
|
669
|
+
}
|
|
670
|
+
async function remove(transport, id) {
|
|
671
|
+
await send(transport, [{ from: `Attachment:${numericId(id)}`, execute: "Delete" }], { throwOnFailure: true });
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/services/guard.ts
|
|
675
|
+
var CHUNK = 100;
|
|
676
|
+
var UNGUARDABLE_TYPES = ["Idea", "IdeaChangeSet", "IdeaStatus", "IdeaComment", "IdeaCategory", "IdeaForum", "IdeaUser"];
|
|
677
|
+
var UnguardableError = class extends Error {
|
|
678
|
+
reasons;
|
|
679
|
+
constructor(reasons) {
|
|
680
|
+
super(`cannot guard this batch: ${reasons.map((r) => `command ${r.index}: ${r.reason}`).join("; ")}`);
|
|
681
|
+
this.name = "UnguardableError";
|
|
682
|
+
this.reasons = reasons;
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
function aliased(command) {
|
|
686
|
+
const keys = Object.keys(command);
|
|
687
|
+
return keys.length === 1 && keys[0].startsWith("@") ? { alias: keys[0], body: command[keys[0]] } : null;
|
|
688
|
+
}
|
|
689
|
+
var bodyOf2 = (c) => aliased(c)?.body ?? c;
|
|
690
|
+
var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
691
|
+
var isCreateValue = (v) => isObject(v) && "AssetType" in v || Array.isArray(v) && v.some(isCreateValue) || isObject(v) && [].concat(v.add ?? []).some(isCreateValue);
|
|
692
|
+
function unguardable(commands) {
|
|
693
|
+
const out = [];
|
|
694
|
+
commands.forEach((c, index) => {
|
|
695
|
+
const a = aliased(c);
|
|
696
|
+
const body = bodyOf2(c);
|
|
697
|
+
const writes = "update" in body || "execute" in body;
|
|
698
|
+
if (a && writes) out.push({ index, reason: `${a.alias} is an aliased ${"update" in body ? "update" : "execute"}: preview does not expand it, so it cannot be read or guarded per asset` });
|
|
699
|
+
else if (!a && writes && typeof body.from === "string" && body.from.startsWith("@")) out.push({ index, reason: `a bare update or execute from an alias (${body.from}) is rejected by the server; alias the command itself` });
|
|
700
|
+
if (isObject(body.update) && Object.values(body.update).some(isCreateValue)) out.push({ index, reason: "an update that creates assets inline: the projection normalises the creates away" });
|
|
701
|
+
});
|
|
702
|
+
return out;
|
|
703
|
+
}
|
|
704
|
+
function perAsset(projected) {
|
|
705
|
+
const body = bodyOf2(projected);
|
|
706
|
+
return typeof body.oid === "string" && ("update" in body || "execute" in body) ? { oid: momentless(body.oid), update: body.update, execute: body.execute } : null;
|
|
707
|
+
}
|
|
708
|
+
var touchedBy = (projected) => [...new Set(projected.flatMap((p) => {
|
|
709
|
+
const x = perAsset(p);
|
|
710
|
+
return x ? [x.oid] : [];
|
|
711
|
+
}))];
|
|
712
|
+
var STAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,7})?$/;
|
|
713
|
+
async function stampsOf(transport, oids) {
|
|
714
|
+
const byType2 = /* @__PURE__ */ new Map();
|
|
715
|
+
for (const oid of new Set(oids.map(momentless))) {
|
|
716
|
+
const t = typeOfOid(oid);
|
|
717
|
+
byType2.set(t, [...byType2.get(t) ?? [], oid]);
|
|
718
|
+
}
|
|
719
|
+
const out = {};
|
|
720
|
+
for (const [type, list3] of byType2) {
|
|
721
|
+
if (UNGUARDABLE_TYPES.includes(type)) continue;
|
|
722
|
+
for (let i = 0; i < list3.length; i += CHUNK) {
|
|
723
|
+
const part = list3.slice(i, i + CHUNK);
|
|
724
|
+
const rows = await query(transport, { from: type, select: ["ChangeDate"], filter: ["ID=$oids"], with: { $oids: part }, page: { size: part.length, start: 0 } });
|
|
725
|
+
for (const r of rows) if (r.ChangeDate != null) out[momentless(r._oid)] = String(r.ChangeDate);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return out;
|
|
729
|
+
}
|
|
730
|
+
function guard(projected, stamps) {
|
|
731
|
+
const commands = [], unstamped = [];
|
|
732
|
+
for (const p of projected) {
|
|
733
|
+
const x = perAsset(p);
|
|
734
|
+
if (!x) {
|
|
735
|
+
commands.push(p);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
const stamp = stamps[x.oid];
|
|
739
|
+
if (stamp === void 0) {
|
|
740
|
+
unstamped.push(x.oid);
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (!STAMP.test(stamp)) throw new Error(`${x.oid}: ChangeDate ${JSON.stringify(stamp)} is not a timestamp; refusing to put it in a filter`);
|
|
744
|
+
const scope = { from: typeOfOid(x.oid), filter: ["ID=$oid", `ChangeDate='${stamp}'`], with: { $oid: x.oid } };
|
|
745
|
+
commands.push(x.update ? { ...scope, update: x.update } : { ...scope, execute: x.execute });
|
|
746
|
+
}
|
|
747
|
+
return { commands, unstamped };
|
|
748
|
+
}
|
|
749
|
+
function passesOf(projected) {
|
|
750
|
+
const seen = /* @__PURE__ */ new Map();
|
|
751
|
+
const passes = [[]];
|
|
752
|
+
for (const p of projected) {
|
|
753
|
+
const x = perAsset(p);
|
|
754
|
+
const k = x ? seen.get(x.oid) ?? 0 : 0;
|
|
755
|
+
if (x) seen.set(x.oid, k + 1);
|
|
756
|
+
(passes[k] ??= []).push(p);
|
|
757
|
+
}
|
|
758
|
+
return passes;
|
|
759
|
+
}
|
|
760
|
+
var GuardedPassError = class extends Error {
|
|
761
|
+
constructor(cause, outcome, sent, pass) {
|
|
762
|
+
super(`guarded send failed in pass ${pass + 1}${pass ? " (earlier passes applied)" : ""}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
763
|
+
this.cause = cause;
|
|
764
|
+
this.outcome = outcome;
|
|
765
|
+
this.sent = sent;
|
|
766
|
+
this.pass = pass;
|
|
767
|
+
this.name = "GuardedPassError";
|
|
768
|
+
}
|
|
769
|
+
cause;
|
|
770
|
+
outcome;
|
|
771
|
+
sent;
|
|
772
|
+
pass;
|
|
773
|
+
};
|
|
774
|
+
async function sendPasses(transport, projected, stamps, options = {}) {
|
|
775
|
+
const passes = passesOf(projected);
|
|
776
|
+
const outcome = emptyOutcome();
|
|
777
|
+
const sent = [], unstamped = [];
|
|
778
|
+
let applied = /* @__PURE__ */ new Set();
|
|
779
|
+
for (const [k, pass] of passes.entries()) {
|
|
780
|
+
let current;
|
|
781
|
+
if (k === 0) current = guard(pass, stamps);
|
|
782
|
+
else {
|
|
783
|
+
const eligible = pass.filter((p) => applied.has(perAsset(p).oid));
|
|
784
|
+
current = guard(eligible, await stampsOf(transport, eligible.map((p) => perAsset(p).oid)));
|
|
785
|
+
}
|
|
786
|
+
const commands = current.commands;
|
|
787
|
+
unstamped.push(...current.unstamped);
|
|
788
|
+
if (!commands.length) break;
|
|
789
|
+
const offset = sent.length;
|
|
790
|
+
let done;
|
|
791
|
+
try {
|
|
792
|
+
const creates = commands.some((c) => "AssetType" in bodyOf2(c));
|
|
793
|
+
done = outcomeOf(await send(transport, commands, { ...options, idempotent: !creates }));
|
|
794
|
+
} catch (error) {
|
|
795
|
+
if (k === 0) throw error;
|
|
796
|
+
throw new GuardedPassError(error, outcome, sent, k);
|
|
797
|
+
}
|
|
798
|
+
sent.push(...commands);
|
|
799
|
+
mergeOutcome(outcome, done, offset);
|
|
800
|
+
applied = new Set([...done.modified, ...done.operated].map(momentless));
|
|
801
|
+
}
|
|
802
|
+
return { outcome, sent, unstamped: [...new Set(unstamped)] };
|
|
803
|
+
}
|
|
804
|
+
async function sendGuarded(transport, commands, options = {}) {
|
|
805
|
+
const refused = unguardable(commands);
|
|
806
|
+
if (refused.length) throw new UnguardableError(refused);
|
|
807
|
+
const projected = await preview(transport, commands);
|
|
808
|
+
const touched = touchedBy(projected);
|
|
809
|
+
const ideas = touched.filter((oid) => UNGUARDABLE_TYPES.includes(typeOfOid(oid)));
|
|
810
|
+
if (ideas.length) throw new UnguardableError(ideas.map((oid) => ({ index: -1, reason: `${oid}: ${typeOfOid(oid)} has no ChangeDate` })));
|
|
811
|
+
const stamps = await stampsOf(transport, touched);
|
|
812
|
+
const { outcome, sent, unstamped } = await sendPasses(transport, projected, stamps, options);
|
|
813
|
+
const done = /* @__PURE__ */ new Set([...outcome.modified, ...outcome.operated]);
|
|
814
|
+
return { outcome, projected, sent, touched, stamps, skipped: [.../* @__PURE__ */ new Set([...unstamped, ...touched.filter((oid) => !done.has(oid))])] };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/services/batch.ts
|
|
818
|
+
var CHUNK2 = 100;
|
|
819
|
+
var bodyOf3 = (c) => aliased(c)?.body ?? c;
|
|
820
|
+
function describe(commands, projected) {
|
|
821
|
+
const out = { creates: [], queries: [], updates: [], executes: [], deferred: [] };
|
|
822
|
+
commands.forEach((c, index) => {
|
|
823
|
+
const a = aliased(c), body = bodyOf3(c);
|
|
824
|
+
if (a && typeof body.from === "string" && ("update" in body || "execute" in body)) out.deferred.push({ index, alias: a.alias, from: body.from, kind: "update" in body ? "update" : "execute" });
|
|
825
|
+
else if (typeof body.AssetType === "string") out.creates.push({ index, alias: a?.alias ?? null, type: body.AssetType, name: typeof body.Name === "string" ? body.Name : null });
|
|
826
|
+
else if (typeof body.from === "string" && !("update" in body) && !("execute" in body)) out.queries.push({ index, from: body.from });
|
|
827
|
+
});
|
|
828
|
+
for (const p of projected) {
|
|
829
|
+
const body = bodyOf3(p);
|
|
830
|
+
if (typeof body.oid !== "string") continue;
|
|
831
|
+
const oid = momentless(body.oid);
|
|
832
|
+
if (body.update && typeof body.update === "object") out.updates.push({ oid, attributes: body.update });
|
|
833
|
+
else if (body.execute !== void 0) {
|
|
834
|
+
const e = body.execute;
|
|
835
|
+
out.executes.push({ oid, op: typeof e === "string" ? e : e.op, args: typeof e === "string" ? {} : e.args ?? {} });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return out;
|
|
839
|
+
}
|
|
840
|
+
var byType = (oids) => {
|
|
841
|
+
const out = /* @__PURE__ */ new Map();
|
|
842
|
+
for (const oid of new Set([...oids].map(momentless))) out.set(typeOfOid(oid), [...out.get(typeOfOid(oid)) ?? [], oid]);
|
|
843
|
+
return out;
|
|
844
|
+
};
|
|
845
|
+
var chunks = (list3) => Array.from({ length: Math.ceil(list3.length / CHUNK2) }, (_, i) => list3.slice(i * CHUNK2, (i + 1) * CHUNK2));
|
|
846
|
+
var plain = (v) => Array.isArray(v) ? v.map(plain) : v && typeof v === "object" && "_oid" in v ? momentless(String(v._oid)) : v ?? null;
|
|
847
|
+
async function namesOf(transport, schema, oids, onFailure) {
|
|
848
|
+
const out = /* @__PURE__ */ new Map();
|
|
849
|
+
for (const [type, list3] of byType(oids)) {
|
|
850
|
+
let select;
|
|
851
|
+
try {
|
|
852
|
+
await schema.load(type);
|
|
853
|
+
select = ["Name", "Number"].filter((a) => schema.attribute(type, a));
|
|
854
|
+
} catch (error) {
|
|
855
|
+
onFailure?.({ type, oids: list3, error });
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
if (!select.length) continue;
|
|
859
|
+
for (const part of chunks(list3)) {
|
|
860
|
+
try {
|
|
861
|
+
const rows = await query(transport, { from: type, select, filter: ["ID=$oids"], with: { $oids: part }, page: { size: part.length, start: 0 } });
|
|
862
|
+
for (const r of rows) out.set(momentless(r._oid), { number: r.Number ?? null, name: r.Name ?? null });
|
|
863
|
+
} catch (error) {
|
|
864
|
+
onFailure?.({ type, oids: part, error });
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return out;
|
|
869
|
+
}
|
|
870
|
+
async function snapshot(transport, described) {
|
|
871
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
872
|
+
const want = (oid, attrs) => {
|
|
873
|
+
const s = wanted.get(oid) ?? /* @__PURE__ */ new Set();
|
|
874
|
+
attrs.forEach((a) => s.add(a));
|
|
875
|
+
wanted.set(oid, s);
|
|
876
|
+
};
|
|
877
|
+
for (const u of described.updates) want(u.oid, [...Object.keys(u.attributes), "ChangeDate"]);
|
|
878
|
+
for (const e of described.executes) want(e.oid, ["AssetState", "ChangeDate"]);
|
|
879
|
+
const before = {};
|
|
880
|
+
for (const [type, oids] of byType(wanted.keys())) {
|
|
881
|
+
const select = [...new Set(oids.flatMap((o) => [...wanted.get(o)]))];
|
|
882
|
+
for (const part of chunks(oids)) {
|
|
883
|
+
const rows = await query(transport, { from: type, select, filter: ["ID=$oids"], with: { $oids: part }, page: { size: part.length, start: 0 } });
|
|
884
|
+
for (const r of rows) {
|
|
885
|
+
const oid = momentless(r._oid), row = r;
|
|
886
|
+
before[oid] = Object.fromEntries([...wanted.get(oid)].map((a) => [a, plain(row[a])]));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return before;
|
|
891
|
+
}
|
|
892
|
+
var INVERSE = { Inactivate: "Reactivate", QuickClose: "Reactivate", Reactivate: "Inactivate", Delete: "Undelete", Undelete: "Delete" };
|
|
893
|
+
var CASCADING = /* @__PURE__ */ new Set(["Inactivate", "QuickClose"]);
|
|
894
|
+
var CASCADE_DEPTH = 8;
|
|
895
|
+
async function openDescendantsOf(transport, schema, roots) {
|
|
896
|
+
const out = {};
|
|
897
|
+
const rootOf = new Map(roots.map((r) => [momentless(r), momentless(r)]));
|
|
898
|
+
const seen = new Set(rootOf.keys());
|
|
899
|
+
let frontier = [...rootOf.keys()];
|
|
900
|
+
for (let depth = 0; depth < CASCADE_DEPTH && frontier.length; depth += 1) {
|
|
901
|
+
const children = /* @__PURE__ */ new Map();
|
|
902
|
+
for (const [type, list3] of byType(frontier)) {
|
|
903
|
+
await schema.load(type);
|
|
904
|
+
if (!schema.attribute(type, "Children")) continue;
|
|
905
|
+
for (const part of chunks(list3)) {
|
|
906
|
+
const rows = await query(transport, { from: type, select: ["Children"], filter: ["ID=$oids"], with: { $oids: part }, page: { size: part.length, start: 0 } });
|
|
907
|
+
for (const r of rows) {
|
|
908
|
+
const parent = momentless(r._oid);
|
|
909
|
+
for (const c of [].concat(plain(r.Children) ?? [])) {
|
|
910
|
+
const child = momentless(String(c));
|
|
911
|
+
if (seen.has(child)) continue;
|
|
912
|
+
seen.add(child);
|
|
913
|
+
children.set(child, rootOf.get(parent));
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const states = children.size ? await statesOf(transport, [...children.keys()]) : {};
|
|
919
|
+
frontier = [];
|
|
920
|
+
for (const [child, root] of children) {
|
|
921
|
+
if (states[child] === void 0 || isClosed(states[child])) continue;
|
|
922
|
+
rootOf.set(child, root);
|
|
923
|
+
(out[root] ??= []).push(child);
|
|
924
|
+
frontier.push(child);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return out;
|
|
928
|
+
}
|
|
929
|
+
function reverseOf(described, before, done, context = {}) {
|
|
930
|
+
const commands = [], notes = [];
|
|
931
|
+
const cannot = (oid, op) => context.canOperate?.(typeOfOid(oid), op) === false;
|
|
932
|
+
const modified = new Set(done.modified.map(momentless)), operated = new Set(done.operated.map(momentless));
|
|
933
|
+
for (const u of described.updates) {
|
|
934
|
+
if (!modified.has(u.oid)) continue;
|
|
935
|
+
const was = before[u.oid];
|
|
936
|
+
if (!was) {
|
|
937
|
+
notes.push(`${u.oid}: no snapshot, cannot restore`);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
const update = {};
|
|
941
|
+
for (const [attr, change] of Object.entries(u.attributes)) {
|
|
942
|
+
const prior = was[attr];
|
|
943
|
+
const c = change && typeof change === "object" && !Array.isArray(change) ? change : {};
|
|
944
|
+
const add2 = [].concat(c.add ?? []), remove2 = [].concat(c.remove ?? []);
|
|
945
|
+
if (Array.isArray(prior)) {
|
|
946
|
+
const added = add2.filter((v) => !prior.includes(v)), removed = remove2.filter((v) => prior.includes(v));
|
|
947
|
+
if (added.length || removed.length) update[attr] = { add: removed, remove: added };
|
|
948
|
+
} else if (prior === void 0) notes.push(`${u.oid}.${attr}: not in snapshot, cannot restore`);
|
|
949
|
+
else update[attr] = prior;
|
|
950
|
+
}
|
|
951
|
+
if (Object.keys(update).length) commands.push({ from: u.oid, update });
|
|
952
|
+
}
|
|
953
|
+
for (const e of described.executes) {
|
|
954
|
+
if (!operated.has(e.oid)) continue;
|
|
955
|
+
const inverse = INVERSE[e.op];
|
|
956
|
+
if (!inverse) {
|
|
957
|
+
notes.push(`${e.oid}: ${e.op} has no known inverse (was ${String(before[e.oid]?.AssetState ?? "?")})`);
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (cannot(e.oid, inverse)) {
|
|
961
|
+
notes.push(`${e.oid}: ${typeOfOid(e.oid)} has no ${inverse} operation, so its ${e.op} is not reversed`);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
commands.push({ from: e.oid, execute: inverse });
|
|
965
|
+
if (!CASCADING.has(e.op)) continue;
|
|
966
|
+
const open = context.openDescendants?.[e.oid] ?? [];
|
|
967
|
+
const closed = context.cascaded ? context.cascaded[e.oid] ?? [] : void 0;
|
|
968
|
+
if (closed === void 0) {
|
|
969
|
+
if (open.length) notes.push(`${e.oid}: ${e.op} may also have closed ${open.length} open descendant${open.length === 1 ? "" : "s"} (${open.join(", ")}); they could not be read back, so they are not reopened`);
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
for (const d of closed) {
|
|
973
|
+
if (cannot(d, "Reactivate")) notes.push(`${d}: closed with ${e.oid}, but ${typeOfOid(d)} has no Reactivate operation`);
|
|
974
|
+
else commands.push({ from: d, execute: "Reactivate" });
|
|
975
|
+
}
|
|
976
|
+
if (closed.length) notes.push(`${e.oid}: ${e.op} also closed ${closed.join(", ")}; reopened after it`);
|
|
977
|
+
}
|
|
978
|
+
for (const oid of [...done.created].reverse().map(momentless)) {
|
|
979
|
+
if (cannot(oid, "Delete")) notes.push(`${oid}: ${typeOfOid(oid)} has no Delete operation, so it is not deleted${typeOfOid(oid) === "Conversation" ? " (the server removes a Conversation once its last Expression is deleted)" : ""}`);
|
|
980
|
+
else commands.push({ from: oid, execute: "Delete" });
|
|
981
|
+
}
|
|
982
|
+
for (const d of described.deferred ?? []) notes.push(`command ${d.index} (${d.alias}): an aliased ${d.kind} of ${d.from}, sent as written with no snapshot; not reversed`);
|
|
983
|
+
if (commands.length) notes.unshift("restores the state recorded at snapshotAt; anything that changed between then and sentAt is not reflected, and a scalar restored to null is cleared, not unset");
|
|
984
|
+
return { commands, notes };
|
|
985
|
+
}
|
|
986
|
+
var BatchRecordError = class extends Error {
|
|
987
|
+
receipt;
|
|
988
|
+
constructor(cause, receipt) {
|
|
989
|
+
super(`the batch was sent and its outcome recorded, but reading the assets back afterwards failed, so receipt.after is incomplete: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
990
|
+
this.name = "BatchRecordError";
|
|
991
|
+
this.receipt = receipt;
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
var BatchSendError = class extends Error {
|
|
995
|
+
receipt;
|
|
996
|
+
constructor(cause, receipt) {
|
|
997
|
+
super(`the batch send failed; whether the server applied it is unknown: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
998
|
+
this.name = "BatchSendError";
|
|
999
|
+
this.receipt = receipt;
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1003
|
+
async function runBatch(transport, schema, commands, options = {}) {
|
|
1004
|
+
const useGuard = options.guard ?? true;
|
|
1005
|
+
if (!commands.length) throw new Error("the batch is empty");
|
|
1006
|
+
const refused = unguardable(commands);
|
|
1007
|
+
const fatal = refused.filter((r) => r.reason.includes("rejected by the server"));
|
|
1008
|
+
if (fatal.length || useGuard && refused.length) throw new UnguardableError(useGuard ? refused : fatal);
|
|
1009
|
+
const notes = [];
|
|
1010
|
+
const noteFailure = ({ type, oids, error }) => notes.push(`could not look up Name for ${oids.length} ${type} asset${oids.length === 1 ? "" : "s"}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1011
|
+
const previewedAt = now();
|
|
1012
|
+
const projected = await preview(transport, commands);
|
|
1013
|
+
const described = describe(commands, projected);
|
|
1014
|
+
const touched = touchedBy(projected);
|
|
1015
|
+
const names = await namesOf(transport, schema, touched, noteFailure);
|
|
1016
|
+
const receipt = { receipt: 1, request: commands, previewedAt, preview: projected, described, names: Object.fromEntries(names), before: {}, sent: false, guarded: false, created: [], touched: [], after: {}, skipped: [], failed: [], openDescendants: {}, cascaded: {}, reverse: { commands: [], notes: [] }, notes };
|
|
1017
|
+
const nothing = !described.creates.length && !described.updates.length && !described.executes.length && !described.queries.length && !described.deferred.length;
|
|
1018
|
+
if (nothing) return receipt;
|
|
1019
|
+
for (const d of described.deferred) notes.push(`command ${d.index} (${d.alias}): an aliased ${d.kind}, which preview does not expand; sent as written, not snapshotted, not reversible`);
|
|
1020
|
+
receipt.snapshotAt = now();
|
|
1021
|
+
receipt.before = await snapshot(transport, described);
|
|
1022
|
+
const closing = described.executes.filter((e) => CASCADING.has(e.op)).map((e) => e.oid);
|
|
1023
|
+
if (closing.length) receipt.openDescendants = await openDescendantsOf(transport, schema, closing);
|
|
1024
|
+
if (options.dryRun) return receipt;
|
|
1025
|
+
if (options.confirm && !await options.confirm(receipt)) return receipt;
|
|
1026
|
+
let unstamped = [];
|
|
1027
|
+
receipt.guarded = useGuard;
|
|
1028
|
+
receipt.sentAt = now();
|
|
1029
|
+
let done;
|
|
1030
|
+
try {
|
|
1031
|
+
if (useGuard) {
|
|
1032
|
+
const stamps = Object.fromEntries(Object.entries(receipt.before).flatMap(([oid, v]) => typeof v.ChangeDate === "string" ? [[oid, v.ChangeDate]] : []));
|
|
1033
|
+
receipt.sentCommands = guard(passesOf(projected)[0], stamps).commands;
|
|
1034
|
+
const result = await sendPasses(transport, projected, stamps);
|
|
1035
|
+
({ outcome: done, unstamped } = result);
|
|
1036
|
+
receipt.sentCommands = result.sent;
|
|
1037
|
+
} else {
|
|
1038
|
+
receipt.sentCommands = commands;
|
|
1039
|
+
done = outcomeOf(await send(transport, commands));
|
|
1040
|
+
}
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
if (error instanceof GuardedPassError) {
|
|
1043
|
+
receipt.sent = true;
|
|
1044
|
+
receipt.outcome = error.outcome;
|
|
1045
|
+
receipt.sentCommands = error.sent;
|
|
1046
|
+
}
|
|
1047
|
+
throw new BatchSendError(error, receipt);
|
|
1048
|
+
}
|
|
1049
|
+
receipt.sent = true;
|
|
1050
|
+
receipt.outcome = done;
|
|
1051
|
+
const hit = [.../* @__PURE__ */ new Set([...done.modified, ...done.operated])];
|
|
1052
|
+
const unnamed = { number: null, name: null };
|
|
1053
|
+
receipt.created = done.created.map((oid, i) => ({ alias: described.creates[i]?.alias ?? null, type: described.creates[i]?.type ?? typeOfOid(oid), oid, ...unnamed }));
|
|
1054
|
+
receipt.touched = hit.map((oid) => ({ oid, ...names.get(oid) ?? unnamed }));
|
|
1055
|
+
const sentAs = receipt.sentCommands ?? [];
|
|
1056
|
+
const oidOfSent = (i) => {
|
|
1057
|
+
const body = i === null ? void 0 : sentAs[i] ? bodyOf3(sentAs[i]) : void 0;
|
|
1058
|
+
const id = body?.with?.$oid ?? body?.from;
|
|
1059
|
+
return typeof id === "string" && id.includes(":") ? momentless(id) : null;
|
|
1060
|
+
};
|
|
1061
|
+
receipt.failed = [...new Set(done.failures.map((f) => oidOfSent(f.index)).filter((oid) => !!oid && touched.includes(oid) && !hit.includes(oid)))];
|
|
1062
|
+
receipt.skipped = useGuard ? [.../* @__PURE__ */ new Set([...unstamped, ...touched.filter((oid) => !hit.includes(oid) && !receipt.failed.includes(oid))])] : [];
|
|
1063
|
+
for (const type of new Set([...done.created, ...hit].map((oid) => typeOfOid(momentless(oid))))) await schema.load(type).catch(() => null);
|
|
1064
|
+
const canOperate = (type, op) => schema.canOperate(type, op);
|
|
1065
|
+
const operatedNow = new Set(done.operated.map(momentless));
|
|
1066
|
+
const closed = Object.keys(receipt.openDescendants).filter((oid) => operatedNow.has(oid) && receipt.openDescendants[oid].length);
|
|
1067
|
+
receipt.reverse = reverseOf(described, receipt.before, done, { openDescendants: receipt.openDescendants, cascaded: closed.length ? void 0 : {}, canOperate });
|
|
1068
|
+
try {
|
|
1069
|
+
if (closed.length) {
|
|
1070
|
+
const states = await statesOf(transport, closed.flatMap((oid) => receipt.openDescendants[oid]));
|
|
1071
|
+
receipt.cascaded = Object.fromEntries(closed.map((oid) => [oid, receipt.openDescendants[oid].filter((d) => isClosed(states[d]))]));
|
|
1072
|
+
receipt.reverse = reverseOf(described, receipt.before, done, { openDescendants: receipt.openDescendants, cascaded: receipt.cascaded, canOperate });
|
|
1073
|
+
}
|
|
1074
|
+
const more = await namesOf(transport, schema, [...done.created, ...hit.filter((oid) => !names.has(oid))], noteFailure);
|
|
1075
|
+
receipt.created = receipt.created.map((c) => ({ ...c, ...more.get(c.oid) ?? unnamed }));
|
|
1076
|
+
receipt.touched = receipt.touched.map((t) => names.has(t.oid) ? t : { ...t, ...more.get(t.oid) ?? unnamed });
|
|
1077
|
+
const all = [.../* @__PURE__ */ new Set([...done.created, ...hit, ...Object.values(receipt.cascaded).flat()])];
|
|
1078
|
+
receipt.after = all.length ? await stampsOf(transport, all) : {};
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
throw new BatchRecordError(error, receipt);
|
|
1081
|
+
}
|
|
1082
|
+
return receipt;
|
|
1083
|
+
}
|
|
1084
|
+
async function statesOf(transport, oids) {
|
|
1085
|
+
const out = {};
|
|
1086
|
+
for (const [type, list3] of byType(oids)) {
|
|
1087
|
+
for (const part of chunks(list3)) {
|
|
1088
|
+
const rows = await query(transport, { from: type, select: ["AssetState"], filter: ["ID=$oids"], with: { $oids: part }, page: { size: part.length, start: 0 } });
|
|
1089
|
+
for (const r of rows) if (r.AssetState != null) out[momentless(r._oid)] = String(r.AssetState);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return out;
|
|
1093
|
+
}
|
|
1094
|
+
var isClosed = (state) => state === "Closed" || state === "128";
|
|
1095
|
+
async function reversible(transport, receipt, options = {}) {
|
|
1096
|
+
const targets = receipt.reverse.commands.map((c) => momentless(String(bodyOf3(c).from)));
|
|
1097
|
+
const now2 = await stampsOf(transport, targets.filter((oid) => receipt.after[oid] !== void 0));
|
|
1098
|
+
const drifted = targets.flatMap((oid) => receipt.after[oid] !== void 0 && now2[oid] !== receipt.after[oid] ? [{ oid, was: receipt.after[oid], now: now2[oid] ?? null }] : []);
|
|
1099
|
+
const skip = new Set(options.force ? [] : drifted.map((d) => d.oid));
|
|
1100
|
+
const kept = receipt.reverse.commands.filter((c) => !skip.has(momentless(String(bodyOf3(c).from))));
|
|
1101
|
+
const deletes = kept.flatMap((c) => bodyOf3(c).execute === "Delete" ? [momentless(String(bodyOf3(c).from))] : []);
|
|
1102
|
+
const states = deletes.length ? await statesOf(transport, deletes) : {};
|
|
1103
|
+
const reactivated = deletes.filter((oid) => isClosed(states[oid]));
|
|
1104
|
+
const commands = kept.flatMap((c) => {
|
|
1105
|
+
const body = bodyOf3(c), oid = momentless(String(body.from));
|
|
1106
|
+
return body.execute === "Delete" && reactivated.includes(oid) ? [{ from: oid, execute: "Reactivate" }, c] : [c];
|
|
1107
|
+
});
|
|
1108
|
+
return { commands, drifted, reactivated };
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// src/types/asset.ts
|
|
1112
|
+
var ASSET_STATE_CODES = { Future: 0, Active: 64, Closed: 128, Dead: 192, Template: 200, OldBrokenDownStory: 208, Deprecated: 254, Deleted: 255 };
|
|
1113
|
+
var STATE_OF_CODE = Object.fromEntries(Object.entries(ASSET_STATE_CODES).map(([name, code]) => [code, name]));
|
|
1114
|
+
function assetStateOf(value) {
|
|
1115
|
+
if (value == null) return null;
|
|
1116
|
+
const code = typeof value === "number" ? value : typeof value === "string" && /^\d+$/.test(value) ? Number(value) : NaN;
|
|
1117
|
+
return Number.isNaN(code) ? String(value) : STATE_OF_CODE[code] ?? String(value);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/services/resolve.ts
|
|
1121
|
+
var NUMBER_ROOTS = {
|
|
1122
|
+
Environment: ["Number", "Name", "AssetState"],
|
|
1123
|
+
ExternalActionInvocation: ["Number"],
|
|
1124
|
+
Goal: ["Number", "Name", "AssetState"],
|
|
1125
|
+
Grant: ["Number", "AssetState"],
|
|
1126
|
+
Issue: ["Number", "Name", "AssetState"],
|
|
1127
|
+
Milestone: ["Number", "Name", "AssetState"],
|
|
1128
|
+
Okr: ["Number", "Name", "AssetState"],
|
|
1129
|
+
OkrSession: ["Number", "Name", "AssetState"],
|
|
1130
|
+
RegressionPlan: ["Number", "Name", "AssetState"],
|
|
1131
|
+
RegressionSuite: ["Number", "Name", "AssetState"],
|
|
1132
|
+
RegressionTest: ["Number", "Name", "AssetState"],
|
|
1133
|
+
Request: ["Number", "Name", "AssetState"],
|
|
1134
|
+
Roadmap: ["Number", "Name", "AssetState"],
|
|
1135
|
+
StrategicTheme: ["Number", "Name", "AssetState"],
|
|
1136
|
+
Topic: ["Number", "Name", "AssetState"],
|
|
1137
|
+
Workitem: ["Number", "Name", "AssetState"]
|
|
1138
|
+
};
|
|
1139
|
+
var isAssetNumber = (text) => /^[A-Za-z]+-\d+$/.test(text.trim());
|
|
1140
|
+
var CHUNK3 = 100;
|
|
1141
|
+
var absentRoots = /* @__PURE__ */ new WeakMap();
|
|
1142
|
+
var absentFor = (transport, types) => {
|
|
1143
|
+
if (types) return { has: (root) => types.types.get(root) === null, add: (root) => {
|
|
1144
|
+
types.types.set(root, null);
|
|
1145
|
+
} };
|
|
1146
|
+
const set = absentRoots.get(transport) ?? /* @__PURE__ */ new Set();
|
|
1147
|
+
absentRoots.set(transport, set);
|
|
1148
|
+
return set;
|
|
1149
|
+
};
|
|
1150
|
+
var UNKNOWN_TYPE = /^Unknown AssetType: (\S+)/;
|
|
1151
|
+
async function askRoots(transport, numbers, types) {
|
|
1152
|
+
const absent = absentFor(transport, types);
|
|
1153
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
1154
|
+
const roots = Object.keys(NUMBER_ROOTS).filter((r) => !absent.has(r));
|
|
1155
|
+
try {
|
|
1156
|
+
const results = await queries(transport, roots.map((from) => ({
|
|
1157
|
+
from,
|
|
1158
|
+
select: [...NUMBER_ROOTS[from]],
|
|
1159
|
+
filter: ["Number=$numbers"],
|
|
1160
|
+
with: { $numbers: numbers },
|
|
1161
|
+
page: { size: numbers.length, start: 0 }
|
|
1162
|
+
})));
|
|
1163
|
+
return results.flat();
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
const unknown = error instanceof CommandFailureError ? error.failures.map((f) => UNKNOWN_TYPE.exec(f.message)?.[1]) : [];
|
|
1166
|
+
if (attempt > 0 || !unknown.length || unknown.some((t) => !t || !roots.includes(t))) throw error;
|
|
1167
|
+
for (const t of unknown) absent.add(t);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
async function resolveNumbers(transport, numbers, options = {}) {
|
|
1172
|
+
const firsts = /* @__PURE__ */ new Map();
|
|
1173
|
+
for (const n of numbers) if (!firsts.has(n.trim().toUpperCase())) firsts.set(n.trim().toUpperCase(), n.trim());
|
|
1174
|
+
const asked = [...firsts.values()];
|
|
1175
|
+
const invalid = asked.filter((n) => !isAssetNumber(n));
|
|
1176
|
+
if (invalid.length) throw new TypeError(`not asset numbers (expected a prefix and digits, S-1234): ${invalid.join(", ")}`);
|
|
1177
|
+
const byNumber = /* @__PURE__ */ new Map();
|
|
1178
|
+
for (let i = 0; i < asked.length; i += CHUNK3) {
|
|
1179
|
+
for (const r of await askRoots(transport, asked.slice(i, i + CHUNK3), options.types)) {
|
|
1180
|
+
if (!r.Number || byNumber.has(r.Number.toUpperCase())) continue;
|
|
1181
|
+
const oid = momentless(r._oid);
|
|
1182
|
+
byNumber.set(r.Number.toUpperCase(), { number: r.Number, oid, type: typeOfOid(oid), name: r.Name ?? null, assetState: assetStateOf(r.AssetState) });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return {
|
|
1186
|
+
found: asked.flatMap((n) => byNumber.get(n.toUpperCase()) ?? []),
|
|
1187
|
+
missing: asked.filter((n) => !byNumber.has(n.toUpperCase()))
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
async function resolveRef(transport, spec, options = {}) {
|
|
1191
|
+
const text = spec.trim();
|
|
1192
|
+
if (isOidToken(text)) {
|
|
1193
|
+
const oid = momentless(text);
|
|
1194
|
+
return { number: null, oid, type: typeOfOid(oid), name: null, assetState: null };
|
|
1195
|
+
}
|
|
1196
|
+
if (isAssetNumber(text)) return (await resolveNumbers(transport, [text], options)).found[0] ?? null;
|
|
1197
|
+
throw new TypeError(`"${spec}" is neither an asset number (S-1234) nor an OID token (Story:1234)`);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/wire/meta.ts
|
|
1201
|
+
var meta_exports = {};
|
|
1202
|
+
__export(meta_exports, {
|
|
1203
|
+
getAssetType: () => getAssetType,
|
|
1204
|
+
getAttributeDefinition: () => getAttributeDefinition,
|
|
1205
|
+
getMetamodel: () => getMetamodel,
|
|
1206
|
+
isMetamodelDocument: () => isMetamodelDocument,
|
|
1207
|
+
toAssetType: () => toAssetType,
|
|
1208
|
+
toAttributeDefinition: () => toAttributeDefinition,
|
|
1209
|
+
toMetamodel: () => toMetamodel
|
|
1210
|
+
});
|
|
1211
|
+
function toAttributeDefinition(doc) {
|
|
1212
|
+
return {
|
|
1213
|
+
_type: "AttributeDefinition",
|
|
1214
|
+
Name: doc.Name,
|
|
1215
|
+
Token: doc.Token,
|
|
1216
|
+
DisplayName: doc.DisplayName,
|
|
1217
|
+
AttributeType: doc.AttributeType,
|
|
1218
|
+
IsReadOnly: doc.IsReadOnly,
|
|
1219
|
+
IsRequired: doc.IsRequired,
|
|
1220
|
+
IsMultivalue: doc.IsMultivalue,
|
|
1221
|
+
IsCanned: doc.IsCanned,
|
|
1222
|
+
IsCustom: doc.IsCustom,
|
|
1223
|
+
...doc.Base ? { Base: doc.Base.tokenref } : {},
|
|
1224
|
+
...doc.ReciprocalRelation ? { ReciprocalRelation: doc.ReciprocalRelation.tokenref } : {},
|
|
1225
|
+
...doc.OrderByAttribute ? { OrderByAttribute: doc.OrderByAttribute.tokenref } : {},
|
|
1226
|
+
...doc.DisplayByAttribute ? { DisplayByAttribute: doc.DisplayByAttribute.tokenref } : {},
|
|
1227
|
+
...doc.RelatedAsset ? { RelatedAsset: doc.RelatedAsset.nameref } : {}
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
function toAssetType(doc) {
|
|
1231
|
+
return {
|
|
1232
|
+
_type: "AssetType",
|
|
1233
|
+
Name: doc.Token,
|
|
1234
|
+
Token: doc.Token,
|
|
1235
|
+
DisplayName: doc.DisplayName,
|
|
1236
|
+
Abstract: doc.Abstract,
|
|
1237
|
+
...doc.Base ? { Base: doc.Base.nameref } : {},
|
|
1238
|
+
...doc.DefaultOrderBy ? { DefaultOrderBy: doc.DefaultOrderBy.tokenref } : {},
|
|
1239
|
+
...doc.DefaultDisplayBy ? { DefaultDisplayBy: doc.DefaultDisplayBy.tokenref } : {},
|
|
1240
|
+
...doc.ShortName ? { ShortName: doc.ShortName.tokenref } : {},
|
|
1241
|
+
Attributes: Object.fromEntries(Object.entries(doc.Attributes ?? {}).map(([key, value]) => [key, toAttributeDefinition(value)])),
|
|
1242
|
+
...doc.Operations ? { Operations: Object.keys(doc.Operations).map((token) => token.slice(token.indexOf(".") + 1)) } : {}
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
function toMetamodel(doc) {
|
|
1246
|
+
return { Version: doc.Version, AssetTypes: Object.fromEntries(Object.entries(doc.AssetTypes).map(([key, value]) => [key, toAssetType(value)])) };
|
|
1247
|
+
}
|
|
1248
|
+
function isMetamodelDocument(value) {
|
|
1249
|
+
return !!value && typeof value === "object" && typeof value.Version === "string" && !!value.AssetTypes && typeof value.AssetTypes === "object";
|
|
1250
|
+
}
|
|
1251
|
+
async function getMetamodel(transport) {
|
|
1252
|
+
return toMetamodel(await transport.request({ method: "GET", path: "/meta.v1", responseType: "json" }));
|
|
1253
|
+
}
|
|
1254
|
+
async function getAssetType(transport, token) {
|
|
1255
|
+
return toAssetType(await transport.request({ method: "GET", path: `/meta.v1/${encodeURIComponent(token)}`, responseType: "json" }));
|
|
1256
|
+
}
|
|
1257
|
+
async function getAttributeDefinition(transport, tokenref) {
|
|
1258
|
+
const [type, attribute] = tokenref.split(".");
|
|
1259
|
+
if (!type || !attribute) throw new Error(`"${tokenref}" is not an attribute token ("Story.Name")`);
|
|
1260
|
+
return toAttributeDefinition(await transport.request({ method: "GET", path: `/meta.v1/${encodeURIComponent(type)}/${encodeURIComponent(attribute)}`, responseType: "json" }));
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// src/services/schema.ts
|
|
1264
|
+
var TypeCache = class _TypeCache {
|
|
1265
|
+
/** Loaded types; null records that the instance has no such type, so it is not asked for again. */
|
|
1266
|
+
types = /* @__PURE__ */ new Map();
|
|
1267
|
+
loading = /* @__PURE__ */ new Map();
|
|
1268
|
+
/** A cache seeded from a metamodel document. */
|
|
1269
|
+
static fromMetamodel(metamodel) {
|
|
1270
|
+
const cache = new _TypeCache();
|
|
1271
|
+
for (const [token, type] of Object.entries(metamodel.AssetTypes)) cache.types.set(token, type);
|
|
1272
|
+
return cache;
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
var Schema = class _Schema {
|
|
1276
|
+
constructor(transport, seed, cache = new TypeCache()) {
|
|
1277
|
+
this.transport = transport;
|
|
1278
|
+
this.cache = cache;
|
|
1279
|
+
this.types = cache.types;
|
|
1280
|
+
this.loading = cache.loading;
|
|
1281
|
+
if (seed) {
|
|
1282
|
+
for (const [token, type] of Object.entries(seed.AssetTypes)) if (!this.types.has(token)) this.types.set(token, type);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
transport;
|
|
1286
|
+
types;
|
|
1287
|
+
loading;
|
|
1288
|
+
/** Where this schema keeps what it loads; pass it to another schema to share it. */
|
|
1289
|
+
cache;
|
|
1290
|
+
/** The same cache, loading anything missing through another transport (another user's credentials). */
|
|
1291
|
+
withTransport(transport) {
|
|
1292
|
+
return new _Schema(transport, void 0, this.cache);
|
|
1293
|
+
}
|
|
1294
|
+
/** A complete schema from a metamodel document; nothing is ever fetched. */
|
|
1295
|
+
static fromMetamodel(metamodel) {
|
|
1296
|
+
return new _Schema(void 0, metamodel);
|
|
1297
|
+
}
|
|
1298
|
+
/** The type, null when the instance has none, undefined when not loaded yet. */
|
|
1299
|
+
assetType(token) {
|
|
1300
|
+
return this.types.has(token) ? this.types.get(token) : void 0;
|
|
1301
|
+
}
|
|
1302
|
+
/** An attribute by bare name; undefined when the type is not loaded, null when it has no such attribute. */
|
|
1303
|
+
attribute(type, name) {
|
|
1304
|
+
return attributeOf(this, type, name);
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Can the type do this operation (Delete, Reactivate, …)? Undefined when
|
|
1308
|
+
* the type is not loaded or its definition carries no operations, so a
|
|
1309
|
+
* caller can tell "no" from "don't know".
|
|
1310
|
+
*/
|
|
1311
|
+
canOperate(type, op) {
|
|
1312
|
+
const t = this.assetType(type);
|
|
1313
|
+
return t?.Operations ? t.Operations.includes(op) : void 0;
|
|
1314
|
+
}
|
|
1315
|
+
/** The tokens loaded so far. */
|
|
1316
|
+
get loaded() {
|
|
1317
|
+
return [...this.types.entries()].filter(([, t]) => t !== null).map(([token]) => token);
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Load one type from meta.v1 (once; concurrent callers share the request).
|
|
1321
|
+
* A type the instance does not have resolves to null and is remembered;
|
|
1322
|
+
* any other failure (auth, network) is thrown, not swallowed.
|
|
1323
|
+
*/
|
|
1324
|
+
async load(token) {
|
|
1325
|
+
if (this.types.has(token)) return this.types.get(token);
|
|
1326
|
+
const inFlight = this.loading.get(token);
|
|
1327
|
+
if (inFlight) return inFlight;
|
|
1328
|
+
if (!this.transport) {
|
|
1329
|
+
this.types.set(token, null);
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
const request = getAssetType(this.transport, token).catch((error) => {
|
|
1333
|
+
if (error instanceof ApiError && error.status === 404) return null;
|
|
1334
|
+
throw error;
|
|
1335
|
+
}).then((type) => {
|
|
1336
|
+
this.types.set(token, type);
|
|
1337
|
+
return type;
|
|
1338
|
+
}).finally(() => this.loading.delete(token));
|
|
1339
|
+
this.loading.set(token, request);
|
|
1340
|
+
return request;
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Load whatever the queries in a batch need to have their rows translated:
|
|
1344
|
+
* the `from` types, then the types behind every relation hop and sub-select,
|
|
1345
|
+
* until nothing more is needed. Non-query commands are ignored.
|
|
1346
|
+
*/
|
|
1347
|
+
async ensureFor(commands) {
|
|
1348
|
+
for (const command of commands) {
|
|
1349
|
+
const query3 = queryOf(command);
|
|
1350
|
+
if (!query3) continue;
|
|
1351
|
+
const type = fromTypeOf(query3.from, commands);
|
|
1352
|
+
if (!type) continue;
|
|
1353
|
+
for (let round = 0; round < 32; round += 1) {
|
|
1354
|
+
const needed = [...typesNeeded(this, query3, type)].filter((t) => !this.types.has(t));
|
|
1355
|
+
if (!needed.length) break;
|
|
1356
|
+
await Promise.all(needed.map((t) => this.load(t)));
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
function queryOf(command) {
|
|
1362
|
+
if (!command || typeof command !== "object") return null;
|
|
1363
|
+
const keys = Object.keys(command);
|
|
1364
|
+
const body = keys.length === 1 && keys[0].startsWith("@") ? command[keys[0]] : command;
|
|
1365
|
+
if (!body || typeof body !== "object" || typeof body.from !== "string") return null;
|
|
1366
|
+
if ("update" in body || "execute" in body || "AssetType" in body) return null;
|
|
1367
|
+
return body;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// src/wire/loc2.ts
|
|
1371
|
+
var loc2_exports = {};
|
|
1372
|
+
__export(loc2_exports, {
|
|
1373
|
+
DEFAULT_MAX_GET_LENGTH: () => DEFAULT_MAX_GET_LENGTH,
|
|
1374
|
+
localize: () => localize,
|
|
1375
|
+
localizeOne: () => localizeOne,
|
|
1376
|
+
toLson: () => toLson
|
|
1377
|
+
});
|
|
1378
|
+
var DEFAULT_MAX_GET_LENGTH = 2e3;
|
|
1379
|
+
var toLson = (tags) => JSON.stringify(tags);
|
|
1380
|
+
async function localize(transport, tags, options = {}) {
|
|
1381
|
+
const unique = [...new Set(tags)];
|
|
1382
|
+
if (!unique.length) return {};
|
|
1383
|
+
const encoded = encodeURIComponent(toLson(unique));
|
|
1384
|
+
const headers = options.culture ? { "Accept-Language": options.culture } : void 0;
|
|
1385
|
+
const result = encoded.length <= (options.maxGetLength ?? DEFAULT_MAX_GET_LENGTH) ? await transport.request({ method: "GET", path: `/loc-2.v1?${encoded}`, headers, responseType: "json" }) : await transport.request({ method: "POST", path: "/loc-2.v1", headers, body: encoded, contentType: "application/x-www-form-urlencoded", responseType: "json", idempotent: true });
|
|
1386
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) throw new Error(`loc-2.v1 answered an array of tags with ${JSON.stringify(result)?.slice(0, 100)}, not an object`);
|
|
1387
|
+
return result;
|
|
1388
|
+
}
|
|
1389
|
+
async function localizeOne(transport, tag, options = {}) {
|
|
1390
|
+
const map = await localize(transport, [tag], options);
|
|
1391
|
+
return map[tag] ?? tag;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// src/wire/queryV1.ts
|
|
1395
|
+
var queryV1_exports = {};
|
|
1396
|
+
__export(queryV1_exports, {
|
|
1397
|
+
queries: () => queries2,
|
|
1398
|
+
query: () => query2
|
|
1399
|
+
});
|
|
1400
|
+
function rejected(error) {
|
|
1401
|
+
const details = error.details;
|
|
1402
|
+
const messages = (details?.exceptions ?? []).map((e) => e?.message).filter((m) => typeof m === "string" && !!m);
|
|
1403
|
+
return messages.length ? new ApiError(messages.join("; "), error.status, "QUERY_REJECTED", error.details) : error;
|
|
1404
|
+
}
|
|
1405
|
+
async function queries2(transport, qs, options = {}) {
|
|
1406
|
+
for (const q of qs) if (!q.page) throw new Error(`query of ${q.from} without a page: every query.v1 read must be bounded`);
|
|
1407
|
+
if (!qs.length) return [];
|
|
1408
|
+
let response;
|
|
1409
|
+
try {
|
|
1410
|
+
response = await transport.request({ method: "POST", path: "/query.v1", body: qs, responseType: "json", idempotent: true });
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
throw error instanceof ApiError && error.status === 400 ? rejected(error) : error;
|
|
1413
|
+
}
|
|
1414
|
+
if (!Array.isArray(response) || response.length !== qs.length || !response.every(Array.isArray)) {
|
|
1415
|
+
throw new ApiError(`query.v1 answered with ${Array.isArray(response) ? `${response.length} results` : "no result list"} for ${qs.length} queries`, 200, "BAD_RESPONSE", response);
|
|
1416
|
+
}
|
|
1417
|
+
return qs.map((q, i) => {
|
|
1418
|
+
const type = options.lookup ? fromTypeOf(q.from, qs) : null;
|
|
1419
|
+
const context = type ? { lookup: options.lookup, type } : void 0;
|
|
1420
|
+
return response[i].map((row) => fromWireRow(row, context));
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
async function query2(transport, q, options = {}) {
|
|
1424
|
+
const [rows] = await queries2(transport, [q], options);
|
|
1425
|
+
return rows ?? [];
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/wire/rest1.ts
|
|
1429
|
+
var rest1_exports = {};
|
|
1430
|
+
__export(rest1_exports, {
|
|
1431
|
+
fromAssetDocument: () => fromAssetDocument,
|
|
1432
|
+
get: () => get2,
|
|
1433
|
+
history: () => history,
|
|
1434
|
+
list: () => list2,
|
|
1435
|
+
momentless: () => momentless,
|
|
1436
|
+
toQuery: () => toQuery
|
|
1437
|
+
});
|
|
1438
|
+
var toRef = (doc) => ({ _oid: momentless(doc.idref) });
|
|
1439
|
+
function fromAssetDocument(doc, { history: history2 = false } = {}) {
|
|
1440
|
+
const out = { _oid: history2 ? doc.id : momentless(doc.id) };
|
|
1441
|
+
const attrs = doc.Attributes ?? {};
|
|
1442
|
+
for (const [key, a] of Object.entries(attrs)) {
|
|
1443
|
+
if (a._type === "Relation") {
|
|
1444
|
+
out[key] = Array.isArray(a.value) ? a.value.map(toRef) : a.value ? toRef(a.value) : null;
|
|
1445
|
+
} else if (key === "AssetState" && typeof a.value === "number") {
|
|
1446
|
+
out[key] = assetStateOf(a.value);
|
|
1447
|
+
} else {
|
|
1448
|
+
out[key] = a.value;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
for (const [key, a] of Object.entries(attrs)) {
|
|
1452
|
+
const dot = key.indexOf(".");
|
|
1453
|
+
if (dot < 0 || a._type !== "Attribute") continue;
|
|
1454
|
+
const rel = key.slice(0, dot), attr = key.slice(dot + 1);
|
|
1455
|
+
if (attr.includes(".")) continue;
|
|
1456
|
+
const target = out[rel];
|
|
1457
|
+
const values = a.value;
|
|
1458
|
+
if (Array.isArray(target) && Array.isArray(values)) target.forEach((ref, i) => {
|
|
1459
|
+
if (ref && typeof ref === "object") ref[attr] = values[i];
|
|
1460
|
+
});
|
|
1461
|
+
else if (target && typeof target === "object" && !Array.isArray(target) && !Array.isArray(a.value)) target[attr] = a.value;
|
|
1462
|
+
}
|
|
1463
|
+
return out;
|
|
1464
|
+
}
|
|
1465
|
+
function toQuery(params) {
|
|
1466
|
+
return {
|
|
1467
|
+
sel: params.sel?.join(","),
|
|
1468
|
+
where: params.where?.length ? params.where.join(";") : void 0,
|
|
1469
|
+
with: params.with ? Object.entries(params.with).map(([k, v]) => `${k}=${[].concat(v).join(",")}`).join("|") : void 0,
|
|
1470
|
+
find: params.find,
|
|
1471
|
+
findin: params.findin?.join(","),
|
|
1472
|
+
sort: params.sort?.join(","),
|
|
1473
|
+
page: `${params.page.size},${params.page.start}`,
|
|
1474
|
+
deleted: params.deleted === void 0 ? void 0 : String(params.deleted),
|
|
1475
|
+
needTotal: params.needTotal === void 0 ? void 0 : String(params.needTotal),
|
|
1476
|
+
asof: params.asof
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
async function read(transport, path, params, history2) {
|
|
1480
|
+
if (!params.page) throw new Error(`rest-1 read of ${path} without a page: every read must be bounded`);
|
|
1481
|
+
const doc = await transport.request({ method: "GET", path, query: toQuery(params), responseType: "json" });
|
|
1482
|
+
return { rows: (doc.Assets ?? []).map((a) => fromAssetDocument(a, { history: history2 })), total: typeof doc.total === "number" && doc.total >= 0 ? doc.total : null, pageSize: doc.pageSize, pageStart: doc.pageStart };
|
|
1483
|
+
}
|
|
1484
|
+
var list2 = (transport, type, params) => read(transport, `/rest-1.v1/Data/${encodeURIComponent(type)}`, params, false);
|
|
1485
|
+
async function get2(transport, oid, sel) {
|
|
1486
|
+
const [type, id] = momentless(oid).split(":");
|
|
1487
|
+
const doc = await transport.request({ method: "GET", path: `/rest-1.v1/Data/${encodeURIComponent(type)}/${encodeURIComponent(id)}`, query: { sel: sel?.join(",") }, responseType: "json" });
|
|
1488
|
+
return fromAssetDocument(doc);
|
|
1489
|
+
}
|
|
1490
|
+
var history = (transport, target, params) => {
|
|
1491
|
+
const [type, id] = target.split(":");
|
|
1492
|
+
const path = id ? `/rest-1.v1/Hist/${encodeURIComponent(type)}/${encodeURIComponent(id)}` : `/rest-1.v1/Hist/${encodeURIComponent(type)}`;
|
|
1493
|
+
return read(transport, path, params, true);
|
|
1494
|
+
};
|
|
1495
|
+
|
|
1496
|
+
// src/client.ts
|
|
1497
|
+
var AgilityApiClient = class _AgilityApiClient {
|
|
1498
|
+
transport;
|
|
1499
|
+
schema;
|
|
1500
|
+
constructor(config, options = {}) {
|
|
1501
|
+
const opts = "request" in options ? { transport: options } : options;
|
|
1502
|
+
this.transport = opts.transport ?? createFetchTransport({
|
|
1503
|
+
baseUrl: config.baseUrl,
|
|
1504
|
+
authorization: config.authorization,
|
|
1505
|
+
accessToken: config.accessToken,
|
|
1506
|
+
timeoutMs: config.timeoutMs,
|
|
1507
|
+
retries: config.retries,
|
|
1508
|
+
defaultHeaders: config.defaultHeaders,
|
|
1509
|
+
credentials: config.credentials,
|
|
1510
|
+
onUnauthorized: config.onUnauthorized,
|
|
1511
|
+
unauthorizedStatuses: config.unauthorizedStatuses
|
|
1512
|
+
});
|
|
1513
|
+
this.config = config;
|
|
1514
|
+
this.schema = opts.schema ?? new Schema(this.transport, opts.metamodel, opts.types);
|
|
1515
|
+
}
|
|
1516
|
+
config;
|
|
1517
|
+
/**
|
|
1518
|
+
* The same client, with every request it sends tied to `signal`: abort it
|
|
1519
|
+
* (on navigation, say) and in-flight requests end with an ApiError coded
|
|
1520
|
+
* ABORTED. The schema and its cache are shared; a type load that other
|
|
1521
|
+
* callers are waiting on is not cancelled.
|
|
1522
|
+
*/
|
|
1523
|
+
withSignal(signal) {
|
|
1524
|
+
const base = this.transport;
|
|
1525
|
+
const transport = { request: (options) => base.request({ ...options, signal: options.signal ?? signal }) };
|
|
1526
|
+
return new _AgilityApiClient(this.config, { transport, schema: this.schema });
|
|
1527
|
+
}
|
|
1528
|
+
/** meta.v1: the instance's schema, raw. (The translated, cached view is `schema`.) */
|
|
1529
|
+
meta = {
|
|
1530
|
+
metamodel: () => getMetamodel(this.transport),
|
|
1531
|
+
assetType: (token) => getAssetType(this.transport, token),
|
|
1532
|
+
attributeDefinition: (tokenref) => getAttributeDefinition(this.transport, tokenref)
|
|
1533
|
+
};
|
|
1534
|
+
/**
|
|
1535
|
+
* loc-2.v1: labels for tags, in one request (meta's DisplayName values are
|
|
1536
|
+
* tags). An unmatched tag comes back as itself; the server never says
|
|
1537
|
+
* "missing", so neither does this.
|
|
1538
|
+
*/
|
|
1539
|
+
localize(tags, options) {
|
|
1540
|
+
return localize(this.transport, tags, options);
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Services: what a person types, turned into assets. Numbers resolve in
|
|
1544
|
+
* one api/asset request across every number root; an OID token is taken
|
|
1545
|
+
* as is.
|
|
1546
|
+
*/
|
|
1547
|
+
resolve = {
|
|
1548
|
+
numbers: (numbers) => resolveNumbers(this.transport, numbers, { types: this.schema.cache }),
|
|
1549
|
+
ref: (spec) => resolveRef(this.transport, spec, { types: this.schema.cache })
|
|
1550
|
+
};
|
|
1551
|
+
/**
|
|
1552
|
+
* A batch with a record: preview, snapshot what it overwrites, (confirm,)
|
|
1553
|
+
* send guarded, and a receipt with created/touched/skipped assets and a
|
|
1554
|
+
* reverse batch. `reversible` drops from a receipt's reverse the assets
|
|
1555
|
+
* changed since, unless forced.
|
|
1556
|
+
*/
|
|
1557
|
+
batch = {
|
|
1558
|
+
run: (commands, options) => runBatch(this.transport, this.schema, commands, options),
|
|
1559
|
+
reversible: (receipt, options) => reversible(this.transport, receipt, options)
|
|
1560
|
+
};
|
|
1561
|
+
/**
|
|
1562
|
+
* The files attached to assets: metadata on Attachment assets, bytes
|
|
1563
|
+
* through attachment.img. `add` sends a small file inline (one request)
|
|
1564
|
+
* and a large one in two steps; `url` is where a browser opens one.
|
|
1565
|
+
*/
|
|
1566
|
+
attachments = {
|
|
1567
|
+
list: (asset, options) => list(this.transport, asset, options),
|
|
1568
|
+
page: (asset, options) => page(this.transport, asset, options),
|
|
1569
|
+
info: (id) => info(this.transport, id),
|
|
1570
|
+
get: (id) => get(this.transport, id),
|
|
1571
|
+
add: (asset, data, options) => add(this.transport, asset, data, options),
|
|
1572
|
+
put: (id, data, contentType) => put(this.transport, id, data, contentType),
|
|
1573
|
+
remove: (id) => remove(this.transport, id),
|
|
1574
|
+
url: (id) => attachmentUrl(this.config.baseUrl, id)
|
|
1575
|
+
};
|
|
1576
|
+
/**
|
|
1577
|
+
* Images embedded in an asset's rich text (Description and other long
|
|
1578
|
+
* text). `upload` returns the URL to put in an <img src>; `normalize`
|
|
1579
|
+
* rewrites every embedded.img reference in some HTML to this instance's
|
|
1580
|
+
* absolute URL, as Agility's own editor stores it. They cannot be deleted.
|
|
1581
|
+
*/
|
|
1582
|
+
embeddedImages = {
|
|
1583
|
+
upload: (asset, data, options) => uploadEmbeddedImage(this.transport, asset, data, options),
|
|
1584
|
+
url: (id) => embeddedImageUrl(this.config.baseUrl, id),
|
|
1585
|
+
normalize: (html) => normalizeEmbeddedImageUrls(html, this.config.baseUrl)
|
|
1586
|
+
};
|
|
1587
|
+
/** rest-1.v1: bounded reads, one asset, history. */
|
|
1588
|
+
data = {
|
|
1589
|
+
list: (type, params) => list2(this.transport, type, params),
|
|
1590
|
+
get: (oid, sel) => get2(this.transport, oid, sel),
|
|
1591
|
+
history: (target, params) => history(this.transport, target, params)
|
|
1592
|
+
};
|
|
1593
|
+
/**
|
|
1594
|
+
* query.v1: reads only, the same query bodies as assets.query, rows
|
|
1595
|
+
* translated the same way. The endpoint cannot write, so it is the safe
|
|
1596
|
+
* place to explore a query; its requests may be retried like a GET.
|
|
1597
|
+
*/
|
|
1598
|
+
queryV1 = {
|
|
1599
|
+
query: async (q) => {
|
|
1600
|
+
await this.schema.ensureFor([q]);
|
|
1601
|
+
return query2(this.transport, q, { lookup: this.schema });
|
|
1602
|
+
},
|
|
1603
|
+
queries: async (qs) => {
|
|
1604
|
+
await this.schema.ensureFor(qs);
|
|
1605
|
+
return queries2(this.transport, qs, { lookup: this.schema });
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
/** api/asset: batches and queries, rows translated through the schema. */
|
|
1609
|
+
assets = {
|
|
1610
|
+
send: async (commands, options) => {
|
|
1611
|
+
await this.schema.ensureFor(commands);
|
|
1612
|
+
return send(this.transport, commands, { ...options, lookup: this.schema });
|
|
1613
|
+
},
|
|
1614
|
+
preview: (commands) => preview(this.transport, commands),
|
|
1615
|
+
/**
|
|
1616
|
+
* Send as a compare-and-set: each asset is changed only if nobody has
|
|
1617
|
+
* changed it since the preview read it; the rest come back `skipped`.
|
|
1618
|
+
* Throws UnguardableError, before any request, for shapes it cannot
|
|
1619
|
+
* cover.
|
|
1620
|
+
*/
|
|
1621
|
+
sendGuarded: async (commands, options) => {
|
|
1622
|
+
await this.schema.ensureFor(commands);
|
|
1623
|
+
return sendGuarded(this.transport, commands, { ...options, lookup: this.schema });
|
|
1624
|
+
},
|
|
1625
|
+
query: async (q) => {
|
|
1626
|
+
await this.schema.ensureFor([q]);
|
|
1627
|
+
return query(this.transport, q, { lookup: this.schema });
|
|
1628
|
+
},
|
|
1629
|
+
queries: async (qs) => {
|
|
1630
|
+
await this.schema.ensureFor(qs);
|
|
1631
|
+
return queries(this.transport, qs, { lookup: this.schema });
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
};
|
|
1635
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1636
|
+
0 && (module.exports = {
|
|
1637
|
+
ASSET_STATE_CODES,
|
|
1638
|
+
AgilityApiClient,
|
|
1639
|
+
ApiError,
|
|
1640
|
+
AttachmentUploadError,
|
|
1641
|
+
BatchRecordError,
|
|
1642
|
+
BatchSendError,
|
|
1643
|
+
CommandFailureError,
|
|
1644
|
+
INLINE_LIMIT,
|
|
1645
|
+
INVERSE,
|
|
1646
|
+
NUMBER_ROOTS,
|
|
1647
|
+
RESOLVE_CHUNK,
|
|
1648
|
+
Schema,
|
|
1649
|
+
TypeCache,
|
|
1650
|
+
UNGUARDABLE_TYPES,
|
|
1651
|
+
UnguardableError,
|
|
1652
|
+
assetApi,
|
|
1653
|
+
assetStateOf,
|
|
1654
|
+
attachment,
|
|
1655
|
+
createFetchTransport,
|
|
1656
|
+
describeBatch,
|
|
1657
|
+
guard,
|
|
1658
|
+
isAssetNumber,
|
|
1659
|
+
isOidToken,
|
|
1660
|
+
loc2,
|
|
1661
|
+
meta,
|
|
1662
|
+
momentless,
|
|
1663
|
+
namesOf,
|
|
1664
|
+
parseOid,
|
|
1665
|
+
queryV1,
|
|
1666
|
+
resolveNumbers,
|
|
1667
|
+
resolveRef,
|
|
1668
|
+
rest1,
|
|
1669
|
+
reverseOf,
|
|
1670
|
+
reversible,
|
|
1671
|
+
runBatch,
|
|
1672
|
+
select,
|
|
1673
|
+
sendGuarded,
|
|
1674
|
+
snapshot,
|
|
1675
|
+
stampsOf,
|
|
1676
|
+
toOid,
|
|
1677
|
+
touchedBy,
|
|
1678
|
+
typeOfOid,
|
|
1679
|
+
unguardable
|
|
1680
|
+
});
|
|
1681
|
+
//# sourceMappingURL=index.cjs.map
|