@odla-ai/chapter 0.47.5 → 0.47.7
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/dist/{copy-context-20WFI5TI.d.ts → copy-context-Dorxp-WK.d.ts} +2 -0
- package/dist/index.cjs +29 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +26 -2
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +303 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +105 -0
- package/dist/testing.d.ts +105 -0
- package/dist/testing.js +280 -0
- package/dist/testing.js.map +1 -0
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +287 -5
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +6 -0
- package/dist/worker/index.d.ts +6 -0
- package/dist/worker/index.js +287 -5
- package/dist/worker/index.js.map +1 -1
- package/package.json +6 -1
package/dist/worker/index.d.cts
CHANGED
|
@@ -11,6 +11,8 @@ interface ChapterTierConfig {
|
|
|
11
11
|
id: string;
|
|
12
12
|
name: string;
|
|
13
13
|
priceCents: number;
|
|
14
|
+
currency?: string;
|
|
15
|
+
interval?: "year" | "month";
|
|
14
16
|
stripePriceId?: string;
|
|
15
17
|
blurb?: string;
|
|
16
18
|
/** Defaults to declaration order when omitted. */
|
|
@@ -946,6 +948,10 @@ interface SignupControlTier {
|
|
|
946
948
|
available: boolean;
|
|
947
949
|
sortOrder: number;
|
|
948
950
|
priceCents: number;
|
|
951
|
+
/** ISO currency; omitted only by pre-field schema-v1 documents. */
|
|
952
|
+
currency?: string;
|
|
953
|
+
/** Recurring interval; omitted only by pre-field schema-v1 documents. */
|
|
954
|
+
interval?: "year" | "month";
|
|
949
955
|
stripePriceId?: string;
|
|
950
956
|
blurb?: string;
|
|
951
957
|
}
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ interface ChapterTierConfig {
|
|
|
11
11
|
id: string;
|
|
12
12
|
name: string;
|
|
13
13
|
priceCents: number;
|
|
14
|
+
currency?: string;
|
|
15
|
+
interval?: "year" | "month";
|
|
14
16
|
stripePriceId?: string;
|
|
15
17
|
blurb?: string;
|
|
16
18
|
/** Defaults to declaration order when omitted. */
|
|
@@ -946,6 +948,10 @@ interface SignupControlTier {
|
|
|
946
948
|
available: boolean;
|
|
947
949
|
sortOrder: number;
|
|
948
950
|
priceCents: number;
|
|
951
|
+
/** ISO currency; omitted only by pre-field schema-v1 documents. */
|
|
952
|
+
currency?: string;
|
|
953
|
+
/** Recurring interval; omitted only by pre-field schema-v1 documents. */
|
|
954
|
+
interval?: "year" | "month";
|
|
949
955
|
stripePriceId?: string;
|
|
950
956
|
blurb?: string;
|
|
951
957
|
}
|
package/dist/worker/index.js
CHANGED
|
@@ -316,6 +316,8 @@ function joinConfig(group, paymentsReady, tiers = [], conditions = {}) {
|
|
|
316
316
|
name: group.name,
|
|
317
317
|
standardPriceCents: group.standardPriceCents ?? 0,
|
|
318
318
|
foundingDiscountCents: group.foundingDiscountCents ?? 0,
|
|
319
|
+
currency: group.currency ?? "usd",
|
|
320
|
+
interval: group.interval ?? "year",
|
|
319
321
|
tiers: [...tiers],
|
|
320
322
|
// The browser evaluates the same conditions the server enforces.
|
|
321
323
|
conditions,
|
|
@@ -348,6 +350,8 @@ function resolveTiers(group, rows5 = []) {
|
|
|
348
350
|
groupId: String(group.id ?? ""),
|
|
349
351
|
name: "Membership",
|
|
350
352
|
priceCents: group.standardPriceCents ?? 0,
|
|
353
|
+
currency: group.currency ?? "usd",
|
|
354
|
+
interval: group.interval ?? "year",
|
|
351
355
|
...group.stripePriceId ? { stripePriceId: group.stripePriceId } : {},
|
|
352
356
|
sortOrder: 0,
|
|
353
357
|
active: true
|
|
@@ -360,11 +364,13 @@ function findTier(tiers, tierId) {
|
|
|
360
364
|
if (!tierId) return tiers.length === 1 ? tiers[0] : null;
|
|
361
365
|
return tiers.find((tier) => tier.id === tierId) ?? null;
|
|
362
366
|
}
|
|
363
|
-
function joinConfigTiers(tiers) {
|
|
367
|
+
function joinConfigTiers(tiers, defaults = {}) {
|
|
364
368
|
return tiers.map((tier) => ({
|
|
365
369
|
id: tier.id,
|
|
366
370
|
name: tier.name,
|
|
367
371
|
priceCents: tier.priceCents,
|
|
372
|
+
currency: tier.currency ?? defaults.currency ?? "usd",
|
|
373
|
+
interval: tier.interval ?? defaults.interval ?? "year",
|
|
368
374
|
blurb: tier.blurb ?? "",
|
|
369
375
|
free: tierIsFree(tier)
|
|
370
376
|
}));
|
|
@@ -377,6 +383,8 @@ function rowToTier(row, groupId) {
|
|
|
377
383
|
groupId: String(row.groupId ?? groupId),
|
|
378
384
|
name: String(row.name ?? ""),
|
|
379
385
|
priceCents: typeof row.priceCents === "number" ? row.priceCents : 0,
|
|
386
|
+
...typeof row.currency === "string" && row.currency ? { currency: row.currency } : {},
|
|
387
|
+
...row.interval === "year" || row.interval === "month" ? { interval: row.interval } : {},
|
|
380
388
|
...typeof row.stripePriceId === "string" && row.stripePriceId ? { stripePriceId: row.stripePriceId } : {},
|
|
381
389
|
...typeof row.blurb === "string" ? { blurb: row.blurb } : {},
|
|
382
390
|
sortOrder: typeof row.sortOrder === "number" ? row.sortOrder : 0,
|
|
@@ -1491,6 +1499,8 @@ function groupAttrs(document) {
|
|
|
1491
1499
|
const firstPaid = [...document.tiers].find((tier) => tier.available && tier.priceCents > 0);
|
|
1492
1500
|
return {
|
|
1493
1501
|
...first4 ? { standardPriceCents: first4.priceCents } : {},
|
|
1502
|
+
currency: first4?.currency ?? "usd",
|
|
1503
|
+
interval: first4?.interval ?? "year",
|
|
1494
1504
|
foundingDiscountCents: 0,
|
|
1495
1505
|
...firstPaid?.stripePriceId ? { stripePriceId: firstPaid.stripePriceId } : {},
|
|
1496
1506
|
...document.policy ?? {},
|
|
@@ -1524,6 +1534,8 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
|
1524
1534
|
groupId: document.target.chapterId,
|
|
1525
1535
|
name: tier.name,
|
|
1526
1536
|
priceCents: tier.priceCents,
|
|
1537
|
+
currency: tier.currency ?? "usd",
|
|
1538
|
+
interval: tier.interval ?? "year",
|
|
1527
1539
|
...tier.stripePriceId ? { stripePriceId: tier.stripePriceId } : {},
|
|
1528
1540
|
...tier.blurb ? { blurb: tier.blurb } : {},
|
|
1529
1541
|
sortOrder: tier.sortOrder,
|
|
@@ -1940,7 +1952,7 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1940
1952
|
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
1941
1953
|
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
1942
1954
|
return json({
|
|
1943
|
-
...joinConfig(group, paymentsReady, joinConfigTiers(offered), application.conditions),
|
|
1955
|
+
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }), application.conditions),
|
|
1944
1956
|
fieldContract: chapterFlowFieldContract(chapter, "join", application),
|
|
1945
1957
|
copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
|
|
1946
1958
|
...activeControl ? { signupControl: {
|
|
@@ -2297,7 +2309,7 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
|
2297
2309
|
"theme",
|
|
2298
2310
|
"giftOffers"
|
|
2299
2311
|
]);
|
|
2300
|
-
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
2312
|
+
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "currency", "interval", "stripePriceId", "blurb"]);
|
|
2301
2313
|
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2302
2314
|
function unknownKeys(value, allowed2, path, errors) {
|
|
2303
2315
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
@@ -2341,6 +2353,12 @@ function tiersOf(value, errors) {
|
|
|
2341
2353
|
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
2342
2354
|
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
2343
2355
|
}
|
|
2356
|
+
if (item.currency !== void 0 && (typeof item.currency !== "string" || !/^[a-z]{3}$/.test(item.currency))) {
|
|
2357
|
+
errors.push(`${path}.currency must be a lowercase ISO currency code`);
|
|
2358
|
+
}
|
|
2359
|
+
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
2360
|
+
errors.push(`${path}.interval must be year or month`);
|
|
2361
|
+
}
|
|
2344
2362
|
const blurb = item.blurb === void 0 ? void 0 : text2(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
2345
2363
|
return [{
|
|
2346
2364
|
id,
|
|
@@ -2348,6 +2366,8 @@ function tiersOf(value, errors) {
|
|
|
2348
2366
|
available: item.available === true,
|
|
2349
2367
|
sortOrder: Number(item.sortOrder) || 0,
|
|
2350
2368
|
priceCents: Number(item.priceCents) || 0,
|
|
2369
|
+
...typeof item.currency === "string" ? { currency: item.currency } : {},
|
|
2370
|
+
...item.interval === "year" || item.interval === "month" ? { interval: item.interval } : {},
|
|
2351
2371
|
...typeof item.stripePriceId === "string" ? { stripePriceId: item.stripePriceId } : {},
|
|
2352
2372
|
...blurb ? { blurb } : {}
|
|
2353
2373
|
}];
|
|
@@ -6874,6 +6894,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
6874
6894
|
// src/network-person-registry.ts
|
|
6875
6895
|
var TOKEN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
|
|
6876
6896
|
var CHAPTER_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
6897
|
+
var FIXTURE_MARKER = /fixture|(?:^|[\s._:-])(dummy|sample|test(?:case)?)(?:$|[\s._:-])/i;
|
|
6877
6898
|
function normalizeNetworkIdentity(kind, value) {
|
|
6878
6899
|
const trimmed = value.trim();
|
|
6879
6900
|
if (kind === "email") return trimmed.toLowerCase();
|
|
@@ -6900,6 +6921,9 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
|
6900
6921
|
if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
|
|
6901
6922
|
if (!provenance || provenance.length > 500) errors.push("provenance is required and bounded");
|
|
6902
6923
|
if (!displayName || displayName.length > 200) errors.push("displayName is required and bounded");
|
|
6924
|
+
if (FIXTURE_MARKER.test(sourceRecordId) || FIXTURE_MARKER.test(displayName)) {
|
|
6925
|
+
errors.push("fixture, test, sample, and dummy rows cannot become network Persons");
|
|
6926
|
+
}
|
|
6903
6927
|
const rawConsent = raw2.consent && typeof raw2.consent === "object" && !Array.isArray(raw2.consent) ? raw2.consent : {};
|
|
6904
6928
|
const consentStates = ["unknown", "granted", "denied"];
|
|
6905
6929
|
const marketing = typeof rawConsent.marketing === "string" ? rawConsent.marketing : "";
|
|
@@ -7312,6 +7336,247 @@ async function suppressNetworkPerson(db, input, mutationId, actor, now = Date.no
|
|
|
7312
7336
|
return next;
|
|
7313
7337
|
}
|
|
7314
7338
|
|
|
7339
|
+
// src/network-person-bulk-preview.ts
|
|
7340
|
+
var NETWORK_PERSON_BULK_IMPORT_MAX_ROWS = 5e3;
|
|
7341
|
+
var NETWORK_PERSON_BULK_IMPORT_VERSION = 1;
|
|
7342
|
+
var networkPersonBulkEmptyCounts = () => ({
|
|
7343
|
+
new: 0,
|
|
7344
|
+
replay: 0,
|
|
7345
|
+
exactMatchReview: 0,
|
|
7346
|
+
invalid: 0,
|
|
7347
|
+
rejected: 0,
|
|
7348
|
+
failed: 0
|
|
7349
|
+
});
|
|
7350
|
+
var incrementNetworkPersonBulkCount = (counts, status) => {
|
|
7351
|
+
if (status === "exact_match_review") counts.exactMatchReview += 1;
|
|
7352
|
+
else counts[status] += 1;
|
|
7353
|
+
};
|
|
7354
|
+
function stableValue(value) {
|
|
7355
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
7356
|
+
if (value && typeof value === "object") {
|
|
7357
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stableValue(item)]));
|
|
7358
|
+
}
|
|
7359
|
+
return value;
|
|
7360
|
+
}
|
|
7361
|
+
async function networkPersonBulkSha256(value) {
|
|
7362
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
7363
|
+
return [...new Uint8Array(bytes)].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
7364
|
+
}
|
|
7365
|
+
function assertNetworkPersonBulkRows(rows5) {
|
|
7366
|
+
if (!Array.isArray(rows5) || rows5.length < 1) {
|
|
7367
|
+
throw new NetworkPersonRegistryError("bulk_rows_required", "bulk import requires at least one row");
|
|
7368
|
+
}
|
|
7369
|
+
if (rows5.length > NETWORK_PERSON_BULK_IMPORT_MAX_ROWS) {
|
|
7370
|
+
throw new NetworkPersonRegistryError(
|
|
7371
|
+
"bulk_rows_exceeded",
|
|
7372
|
+
`bulk import is limited to ${NETWORK_PERSON_BULK_IMPORT_MAX_ROWS.toLocaleString()} rows`
|
|
7373
|
+
);
|
|
7374
|
+
}
|
|
7375
|
+
return rows5;
|
|
7376
|
+
}
|
|
7377
|
+
async function networkPersonBulkBatchDigest(rows5) {
|
|
7378
|
+
const bounded = assertNetworkPersonBulkRows(rows5);
|
|
7379
|
+
return networkPersonBulkSha256(JSON.stringify(stableValue({ version: NETWORK_PERSON_BULK_IMPORT_VERSION, rows: bounded })));
|
|
7380
|
+
}
|
|
7381
|
+
async function networkPersonRegistryStateDigest(state) {
|
|
7382
|
+
const snapshot = {
|
|
7383
|
+
persons: state.persons.map(({ id, revision, state: personState, mergedIntoId }) => ({ id, revision, state: personState, mergedIntoId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
7384
|
+
claims: state.claims.map(({ id, personId }) => ({ id, personId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
7385
|
+
aliases: state.aliases.map(({ id, revision, status, personId }) => ({ id, revision, status, personId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
7386
|
+
links: state.links.map(({ id, personId, state: linkState }) => ({ id, personId, state: linkState })).sort((a, b) => a.id.localeCompare(b.id))
|
|
7387
|
+
};
|
|
7388
|
+
return networkPersonBulkSha256(JSON.stringify(stableValue(snapshot)));
|
|
7389
|
+
}
|
|
7390
|
+
function rejectedValidation(errors) {
|
|
7391
|
+
return errors.some((error) => /approved|real person|non-development|development or invalid email|fixture, test, sample/i.test(error));
|
|
7392
|
+
}
|
|
7393
|
+
async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
|
|
7394
|
+
const aliasIds = new Set(state.aliases.map((alias) => alias.id));
|
|
7395
|
+
const batchAliases = /* @__PURE__ */ new Set();
|
|
7396
|
+
const claimOwners = /* @__PURE__ */ new Map();
|
|
7397
|
+
for (const claim of state.claims) {
|
|
7398
|
+
const owners = claimOwners.get(claim.id) ?? /* @__PURE__ */ new Set();
|
|
7399
|
+
owners.add(claim.personId);
|
|
7400
|
+
claimOwners.set(claim.id, owners);
|
|
7401
|
+
}
|
|
7402
|
+
const results = [];
|
|
7403
|
+
const values = [];
|
|
7404
|
+
for (let index2 = 0; index2 < rows5.length; index2 += 1) {
|
|
7405
|
+
const validation = validateNetworkPersonImport(rows5[index2], allowedChapterIds);
|
|
7406
|
+
if (!validation.ok || !validation.value) {
|
|
7407
|
+
const errors = validation.errors.slice(0, 8);
|
|
7408
|
+
results.push({ rowNumber: index2 + 2, status: rejectedValidation(errors) ? "rejected" : "invalid", errors });
|
|
7409
|
+
values.push(null);
|
|
7410
|
+
continue;
|
|
7411
|
+
}
|
|
7412
|
+
const value = validation.value;
|
|
7413
|
+
const aliasId = await networkSourceKey(value.sourceSystem, value.sourceRecordId);
|
|
7414
|
+
if (batchAliases.has(aliasId)) {
|
|
7415
|
+
results.push({ rowNumber: index2 + 2, status: "rejected", errors: ["duplicate source record in this batch"] });
|
|
7416
|
+
values.push(null);
|
|
7417
|
+
continue;
|
|
7418
|
+
}
|
|
7419
|
+
batchAliases.add(aliasId);
|
|
7420
|
+
if (aliasIds.has(aliasId)) {
|
|
7421
|
+
results.push({ rowNumber: index2 + 2, status: "replay" });
|
|
7422
|
+
values.push(value);
|
|
7423
|
+
continue;
|
|
7424
|
+
}
|
|
7425
|
+
const claims = await Promise.all(value.identities.map(networkIdentityClaim));
|
|
7426
|
+
const candidates = new Set(claims.flatMap((claim) => [...claimOwners.get(claim.id) ?? []]));
|
|
7427
|
+
results.push(candidates.size ? { rowNumber: index2 + 2, status: "exact_match_review", candidateCount: candidates.size } : { rowNumber: index2 + 2, status: "new" });
|
|
7428
|
+
const plannedOwner = `bulk_row_${index2 + 2}`;
|
|
7429
|
+
for (const claim of claims) {
|
|
7430
|
+
const owners = claimOwners.get(claim.id) ?? /* @__PURE__ */ new Set();
|
|
7431
|
+
owners.add(plannedOwner);
|
|
7432
|
+
claimOwners.set(claim.id, owners);
|
|
7433
|
+
}
|
|
7434
|
+
values.push(value);
|
|
7435
|
+
}
|
|
7436
|
+
return { rows: results, values };
|
|
7437
|
+
}
|
|
7438
|
+
async function previewNetworkPersonBulkImport(db, rows5, allowedChapterIds) {
|
|
7439
|
+
const bounded = assertNetworkPersonBulkRows(rows5);
|
|
7440
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7441
|
+
const classified = await classifyNetworkPersonBulkRows(bounded, allowedChapterIds, state);
|
|
7442
|
+
const counts = networkPersonBulkEmptyCounts();
|
|
7443
|
+
for (const row of classified.rows) incrementNetworkPersonBulkCount(counts, row.status);
|
|
7444
|
+
return {
|
|
7445
|
+
version: NETWORK_PERSON_BULK_IMPORT_VERSION,
|
|
7446
|
+
batchDigest: await networkPersonBulkBatchDigest(bounded),
|
|
7447
|
+
stateDigest: await networkPersonRegistryStateDigest(state),
|
|
7448
|
+
rowCount: bounded.length,
|
|
7449
|
+
counts,
|
|
7450
|
+
rows: classified.rows
|
|
7451
|
+
};
|
|
7452
|
+
}
|
|
7453
|
+
|
|
7454
|
+
// src/network-person-bulk-import.ts
|
|
7455
|
+
async function derivedMutationId(batchMutationId, part) {
|
|
7456
|
+
return `bulk_${await networkPersonBulkSha256(`${batchMutationId}\0${part}`)}`;
|
|
7457
|
+
}
|
|
7458
|
+
function matchingEvent(events, mutationId, kind) {
|
|
7459
|
+
return events.find((event) => event.mutationId === mutationId && event.kind === kind);
|
|
7460
|
+
}
|
|
7461
|
+
function eventString(event, key) {
|
|
7462
|
+
return typeof event.detail[key] === "string" ? String(event.detail[key]) : "";
|
|
7463
|
+
}
|
|
7464
|
+
function eventCounts(event) {
|
|
7465
|
+
const value = event.detail.counts;
|
|
7466
|
+
return value && typeof value === "object" && !Array.isArray(value) ? { ...networkPersonBulkEmptyCounts(), ...value } : networkPersonBulkEmptyCounts();
|
|
7467
|
+
}
|
|
7468
|
+
async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, allowedChapterIds, now = Date.now()) {
|
|
7469
|
+
if (input.reviewed !== true) throw new NetworkPersonRegistryError("bulk_review_required", "the exact preview must be explicitly reviewed before apply");
|
|
7470
|
+
const rows5 = assertNetworkPersonBulkRows(input.rows);
|
|
7471
|
+
const batchDigest = await networkPersonBulkBatchDigest(rows5);
|
|
7472
|
+
if (!/^[a-f0-9]{64}$/.test(input.expectedBatchDigest) || batchDigest !== input.expectedBatchDigest) {
|
|
7473
|
+
throw new NetworkPersonRegistryError("bulk_digest_changed", "bulk import rows changed after preview; preview again");
|
|
7474
|
+
}
|
|
7475
|
+
if (!/^[a-f0-9]{64}$/.test(input.expectedStateDigest)) {
|
|
7476
|
+
throw new NetworkPersonRegistryError("bulk_state_digest_required", "a reviewed registry state digest is required");
|
|
7477
|
+
}
|
|
7478
|
+
const startedMutationId = await derivedMutationId(batchMutationId, "started");
|
|
7479
|
+
const completedMutationId = await derivedMutationId(batchMutationId, "completed");
|
|
7480
|
+
let state = await loadNetworkPersonRegistry(db);
|
|
7481
|
+
const completed = matchingEvent(state.events, completedMutationId, "bulk_import_completed");
|
|
7482
|
+
if (completed) {
|
|
7483
|
+
if (eventString(completed, "batchDigest") !== batchDigest) {
|
|
7484
|
+
throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id was already used for different rows");
|
|
7485
|
+
}
|
|
7486
|
+
return {
|
|
7487
|
+
version: NETWORK_PERSON_BULK_IMPORT_VERSION,
|
|
7488
|
+
batchDigest,
|
|
7489
|
+
stateDigest: eventString(completed, "stateDigest"),
|
|
7490
|
+
rowCount: Number(completed.detail.rowCount) || rows5.length,
|
|
7491
|
+
counts: eventCounts(completed),
|
|
7492
|
+
rows: [],
|
|
7493
|
+
replayed: true
|
|
7494
|
+
};
|
|
7495
|
+
}
|
|
7496
|
+
let started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
|
|
7497
|
+
if (!started) {
|
|
7498
|
+
const preview = await previewNetworkPersonBulkImport(db, rows5, allowedChapterIds);
|
|
7499
|
+
if (preview.stateDigest !== input.expectedStateDigest) {
|
|
7500
|
+
throw new NetworkPersonRegistryError("bulk_state_changed", "network Person registry changed after preview; preview again");
|
|
7501
|
+
}
|
|
7502
|
+
try {
|
|
7503
|
+
await db.transact([
|
|
7504
|
+
networkPersonEventOp("bulk_import_started", startedMutationId, actor, now, {
|
|
7505
|
+
batchDigest,
|
|
7506
|
+
stateDigest: input.expectedStateDigest,
|
|
7507
|
+
rowCount: rows5.length,
|
|
7508
|
+
previewCounts: preview.counts
|
|
7509
|
+
})
|
|
7510
|
+
], {
|
|
7511
|
+
mutationId: startedMutationId,
|
|
7512
|
+
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${startedMutationId}`, exists: false }]
|
|
7513
|
+
});
|
|
7514
|
+
} catch (error) {
|
|
7515
|
+
if (!(error instanceof Error) || error.code !== "transact_guard_failed") throw error;
|
|
7516
|
+
}
|
|
7517
|
+
state = await loadNetworkPersonRegistry(db);
|
|
7518
|
+
started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
|
|
7519
|
+
}
|
|
7520
|
+
if (!started || eventString(started, "batchDigest") !== batchDigest || eventString(started, "stateDigest") !== input.expectedStateDigest) {
|
|
7521
|
+
throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id does not match the reviewed preview");
|
|
7522
|
+
}
|
|
7523
|
+
const classified = await classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state);
|
|
7524
|
+
const results = [];
|
|
7525
|
+
const counts = networkPersonBulkEmptyCounts();
|
|
7526
|
+
for (let index2 = 0; index2 < classified.rows.length; index2 += 1) {
|
|
7527
|
+
const row = classified.rows[index2];
|
|
7528
|
+
const value = classified.values[index2];
|
|
7529
|
+
if (!value) {
|
|
7530
|
+
results.push(row);
|
|
7531
|
+
incrementNetworkPersonBulkCount(counts, row.status);
|
|
7532
|
+
continue;
|
|
7533
|
+
}
|
|
7534
|
+
try {
|
|
7535
|
+
const result = await importNetworkPerson(
|
|
7536
|
+
db,
|
|
7537
|
+
value,
|
|
7538
|
+
await derivedMutationId(batchMutationId, `row_${index2 + 2}`),
|
|
7539
|
+
actor,
|
|
7540
|
+
now + index2 + 1
|
|
7541
|
+
);
|
|
7542
|
+
const status = "replayed" in result && result.replayed ? "replay" : result.status === "ambiguous" ? "exact_match_review" : "new";
|
|
7543
|
+
const applied = {
|
|
7544
|
+
rowNumber: index2 + 2,
|
|
7545
|
+
status,
|
|
7546
|
+
...result.status === "ambiguous" ? { candidateCount: result.alias.candidatePersonIds?.length ?? 0 } : {}
|
|
7547
|
+
};
|
|
7548
|
+
results.push(applied);
|
|
7549
|
+
incrementNetworkPersonBulkCount(counts, status);
|
|
7550
|
+
} catch (error) {
|
|
7551
|
+
const failed = {
|
|
7552
|
+
rowNumber: index2 + 2,
|
|
7553
|
+
status: "failed",
|
|
7554
|
+
errors: [error instanceof Error ? error.message : "row failed"]
|
|
7555
|
+
};
|
|
7556
|
+
results.push(failed);
|
|
7557
|
+
incrementNetworkPersonBulkCount(counts, failed.status);
|
|
7558
|
+
}
|
|
7559
|
+
}
|
|
7560
|
+
if (counts.failed === 0) {
|
|
7561
|
+
await db.transact([
|
|
7562
|
+
networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now + rows5.length + 1, {
|
|
7563
|
+
batchDigest,
|
|
7564
|
+
stateDigest: input.expectedStateDigest,
|
|
7565
|
+
rowCount: rows5.length,
|
|
7566
|
+
counts
|
|
7567
|
+
})
|
|
7568
|
+
], { mutationId: completedMutationId });
|
|
7569
|
+
}
|
|
7570
|
+
return {
|
|
7571
|
+
version: NETWORK_PERSON_BULK_IMPORT_VERSION,
|
|
7572
|
+
batchDigest,
|
|
7573
|
+
stateDigest: input.expectedStateDigest,
|
|
7574
|
+
rowCount: rows5.length,
|
|
7575
|
+
counts,
|
|
7576
|
+
rows: results
|
|
7577
|
+
};
|
|
7578
|
+
}
|
|
7579
|
+
|
|
7315
7580
|
// src/network-person-route-projection.ts
|
|
7316
7581
|
function redactedName(displayName) {
|
|
7317
7582
|
return displayName.split(/\s+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}\u2026`).join(" ");
|
|
@@ -7428,7 +7693,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
7428
7693
|
|
|
7429
7694
|
// src/worker-routes-network-person-registry.ts
|
|
7430
7695
|
var PATH2 = "/api/admin/network-person-registry";
|
|
7431
|
-
var MAX_BODY2 =
|
|
7696
|
+
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
7432
7697
|
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7433
7698
|
var positiveRevision = (value) => {
|
|
7434
7699
|
const parsed = Number(value);
|
|
@@ -7443,7 +7708,7 @@ async function gate10(req, env, ctx) {
|
|
|
7443
7708
|
}
|
|
7444
7709
|
function registryError(error) {
|
|
7445
7710
|
if (error instanceof NetworkPersonRegistryError) {
|
|
7446
|
-
const status = error.code.includes("stale") || error.code === "identity_conflict" ? 409 : error.code.endsWith("not_found") ? 404 : 400;
|
|
7711
|
+
const status = error.code.includes("stale") || error.code === "identity_conflict" || error.code === "bulk_state_changed" || error.code === "bulk_digest_changed" || error.code === "bulk_mutation_conflict" ? 409 : error.code.endsWith("not_found") ? 404 : 400;
|
|
7447
7712
|
return json({ error: error.message, code: error.code }, status);
|
|
7448
7713
|
}
|
|
7449
7714
|
if (error instanceof Error && error.code === "transact_guard_failed") {
|
|
@@ -7478,6 +7743,23 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7478
7743
|
if (!MUTATION_ID4.test(mutationId)) return json({ error: "a valid mutationId is required" }, 400);
|
|
7479
7744
|
const actionActor = { id: got.user.userId };
|
|
7480
7745
|
try {
|
|
7746
|
+
if (command === "preview-bulk-import") {
|
|
7747
|
+
const result = await previewNetworkPersonBulkImport(
|
|
7748
|
+
db,
|
|
7749
|
+
input.rows,
|
|
7750
|
+
ctx.chapter.networkPersonRegistry.chapterIds
|
|
7751
|
+
);
|
|
7752
|
+
return json({ result });
|
|
7753
|
+
}
|
|
7754
|
+
if (command === "apply-bulk-import") {
|
|
7755
|
+
const result = await applyNetworkPersonBulkImport(db, {
|
|
7756
|
+
rows: input.rows,
|
|
7757
|
+
expectedBatchDigest: typeof input.expectedBatchDigest === "string" ? input.expectedBatchDigest : "",
|
|
7758
|
+
expectedStateDigest: typeof input.expectedStateDigest === "string" ? input.expectedStateDigest : "",
|
|
7759
|
+
reviewed: input.reviewed === true
|
|
7760
|
+
}, mutationId, actionActor, ctx.chapter.networkPersonRegistry.chapterIds);
|
|
7761
|
+
return json({ result }, result.replayed ? 200 : 201);
|
|
7762
|
+
}
|
|
7481
7763
|
if (command === "import") {
|
|
7482
7764
|
const validated = validateNetworkPersonImport(input, ctx.chapter.networkPersonRegistry.chapterIds);
|
|
7483
7765
|
if (!validated.ok || !validated.value) return json({ error: "invalid import", details: validated.errors }, 400);
|