@odla-ai/chapter 0.25.8 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -13
- package/dist/{chunk-4FW35E3P.js → chunk-4QZEAWQL.js} +170 -45
- package/dist/chunk-4QZEAWQL.js.map +1 -0
- package/dist/{chunk-WGQO4WDJ.js → chunk-OIWCKF2G.js} +20 -3
- package/dist/chunk-OIWCKF2G.js.map +1 -0
- package/dist/{chunk-WHA3MUDQ.js → chunk-QBTFQUDL.js} +2 -2
- package/dist/{copy-context-DI21CYQ3.d.ts → copy-context-CLvhJhrF.d.ts} +73 -42
- package/dist/index.cjs +327 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +170 -58
- package/dist/index.d.ts +170 -58
- package/dist/index.js +321 -20
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +8 -3
- package/dist/ui/admin/index.js +4 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +5 -3
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +496 -205
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +73 -42
- package/dist/worker/index.d.ts +73 -42
- package/dist/worker/index.js +502 -206
- package/dist/worker/index.js.map +1 -1
- package/package.json +3 -3
- package/runbooks/adopt-existing.md +12 -1
- package/runbooks/greenfield.md +27 -6
- package/dist/chunk-4FW35E3P.js.map +0 -1
- package/dist/chunk-WGQO4WDJ.js.map +0 -1
- /package/dist/{chunk-WHA3MUDQ.js.map → chunk-QBTFQUDL.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -519,12 +519,17 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
519
519
|
overview: "Overview",
|
|
520
520
|
billing: "Billing",
|
|
521
521
|
people: "People",
|
|
522
|
+
portfolio: "Portfolio",
|
|
522
523
|
settings: "Settings",
|
|
523
524
|
calendar: "Calendar",
|
|
524
525
|
email: "Email",
|
|
526
|
+
chapters: "Chapters",
|
|
527
|
+
formations: "Formation applications",
|
|
528
|
+
dealFlow: "Deal flow",
|
|
525
529
|
dashboardViewsLabel: "Dashboard views",
|
|
526
530
|
settingsViewsLabel: "Settings views",
|
|
527
|
-
collectionsLabel: "CRM collections"
|
|
531
|
+
collectionsLabel: "CRM collections",
|
|
532
|
+
portfolioViewsLabel: "Portfolio views"
|
|
528
533
|
},
|
|
529
534
|
dashboard: {
|
|
530
535
|
loading: "Loading dashboard\u2026",
|
|
@@ -630,7 +635,19 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
630
635
|
shareWith: "Share with {target}",
|
|
631
636
|
shared: "Shared with {target}.",
|
|
632
637
|
shareFailed: "Could not share with {target}.",
|
|
633
|
-
deliveryFailed: "Delivery failed."
|
|
638
|
+
deliveryFailed: "Delivery failed.",
|
|
639
|
+
loadingRollup: "Loading network overview\u2026",
|
|
640
|
+
loadRollupFailed: "Couldn't load the network overview",
|
|
641
|
+
overview: "Network overview",
|
|
642
|
+
configuredFollowers: "Configured followers",
|
|
643
|
+
reachableFollowers: "Reachable followers",
|
|
644
|
+
recordsAcrossFollowers: "Records across followers",
|
|
645
|
+
noFollowers: "No follower sites are configured.",
|
|
646
|
+
followers: "Followers",
|
|
647
|
+
available: "available",
|
|
648
|
+
unavailable: "unavailable",
|
|
649
|
+
recordTotals: "Portfolio records",
|
|
650
|
+
noRecordTotals: "No aggregate records are available yet."
|
|
634
651
|
},
|
|
635
652
|
records: {
|
|
636
653
|
workflowMissing: "This record is not linked to an application workflow.",
|
|
@@ -788,6 +805,81 @@ function formatChapterCopy(template, values) {
|
|
|
788
805
|
);
|
|
789
806
|
}
|
|
790
807
|
|
|
808
|
+
// src/formation.ts
|
|
809
|
+
var DEFAULT_REQUIRED2 = ["name", "email", "location", "thesis"];
|
|
810
|
+
var DEFAULT_OPTIONAL2 = ["proposedChapter", "experience", "collaborators"];
|
|
811
|
+
function resolveLeaderFormation(input, crm) {
|
|
812
|
+
const type = input?.type ?? "chapter_application";
|
|
813
|
+
if (!input) {
|
|
814
|
+
return {
|
|
815
|
+
enabled: false,
|
|
816
|
+
type,
|
|
817
|
+
required: [],
|
|
818
|
+
optional: [],
|
|
819
|
+
maxLen: {},
|
|
820
|
+
defaultMaxLen: 4e3,
|
|
821
|
+
bodyCap: 32768
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
const def = crm.config.types[type];
|
|
825
|
+
if (!def) throw new Error(`defineChapter.formation.type: unknown CRM type "${type}"`);
|
|
826
|
+
const required = input.required ?? (type === "chapter_application" ? DEFAULT_REQUIRED2 : []);
|
|
827
|
+
const optional = input.optional ?? (type === "chapter_application" ? DEFAULT_OPTIONAL2 : []);
|
|
828
|
+
for (const [name, values] of [["required", required], ["optional", optional]]) {
|
|
829
|
+
if (!Array.isArray(values) || !values.every((field) => typeof field === "string" && field !== "")) {
|
|
830
|
+
throw new Error(`defineChapter.formation.${name}: must be an array of field-name strings`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
const duplicates = [...required, ...optional].filter((field, index, fields) => fields.indexOf(field) !== index);
|
|
834
|
+
if (duplicates.length) throw new Error(`defineChapter.formation: duplicate public field "${duplicates[0]}"`);
|
|
835
|
+
const unknown = [...required, ...optional].filter((field) => !def.fields[field]);
|
|
836
|
+
if (unknown.length) throw new Error(`defineChapter.formation: field "${unknown[0]}" is not declared on CRM type "${type}"`);
|
|
837
|
+
const missingRequired = Object.entries(def.fields).filter(([, field]) => field.required).map(([field]) => field).filter((field) => !required.includes(field));
|
|
838
|
+
if (missingRequired.length) {
|
|
839
|
+
throw new Error(`defineChapter.formation.required: must include required CRM field "${missingRequired[0]}"`);
|
|
840
|
+
}
|
|
841
|
+
const unsupportedRequired = required.find((field) => !["string", "email", "url", "enum"].includes(def.fields[field].type));
|
|
842
|
+
if (unsupportedRequired) {
|
|
843
|
+
throw new Error(`defineChapter.formation.required: field "${unsupportedRequired}" must be string-like`);
|
|
844
|
+
}
|
|
845
|
+
if (required.length === 0) throw new Error("defineChapter.formation.required: must not be empty");
|
|
846
|
+
const defaultMaxLen = input.defaultMaxLen ?? 4e3;
|
|
847
|
+
const bodyCap = input.bodyCap ?? 32768;
|
|
848
|
+
if (!Number.isSafeInteger(defaultMaxLen) || defaultMaxLen < 1 || defaultMaxLen > 32768) {
|
|
849
|
+
throw new Error("defineChapter.formation.defaultMaxLen: must be an integer from 1 to 32768");
|
|
850
|
+
}
|
|
851
|
+
if (!Number.isSafeInteger(bodyCap) || bodyCap < 1024 || bodyCap > 131072) {
|
|
852
|
+
throw new Error("defineChapter.formation.bodyCap: must be an integer from 1024 to 131072");
|
|
853
|
+
}
|
|
854
|
+
for (const [field, cap] of Object.entries(input.maxLen ?? {})) {
|
|
855
|
+
if (![...required, ...optional].includes(field) || !Number.isSafeInteger(cap) || cap < 1 || cap > 32768) {
|
|
856
|
+
throw new Error(`defineChapter.formation.maxLen.${field}: must name a public field and be an integer from 1 to 32768`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return {
|
|
860
|
+
enabled: true,
|
|
861
|
+
type,
|
|
862
|
+
required,
|
|
863
|
+
optional,
|
|
864
|
+
maxLen: input.maxLen ?? {},
|
|
865
|
+
defaultMaxLen,
|
|
866
|
+
bodyCap
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
function formationFields(crm, formation) {
|
|
870
|
+
const def = crm.type(formation.type);
|
|
871
|
+
return [...formation.required, ...formation.optional].map((id2) => {
|
|
872
|
+
const field = def.fields[id2];
|
|
873
|
+
return {
|
|
874
|
+
id: id2,
|
|
875
|
+
label: field.label ?? id2,
|
|
876
|
+
type: field.type,
|
|
877
|
+
required: formation.required.includes(id2),
|
|
878
|
+
...field.options ? { options: field.options } : {}
|
|
879
|
+
};
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
791
883
|
// src/config.ts
|
|
792
884
|
var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
793
885
|
var FIELD = /^[a-z][a-zA-Z0-9_]*$/;
|
|
@@ -876,6 +968,7 @@ function defineChapter(config) {
|
|
|
876
968
|
const brand = { ...config.brand, wordmark: config.brand?.wordmark ?? name };
|
|
877
969
|
const copy = resolveChapterCopy(config.copy);
|
|
878
970
|
const network = resolveNetwork(config, crm);
|
|
971
|
+
const formation = resolveLeaderFormation(config.formation, crm);
|
|
879
972
|
const { schema, rules } = chapterDb(mode, auth);
|
|
880
973
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
881
974
|
const account = config.account ?? "none";
|
|
@@ -931,6 +1024,7 @@ function defineChapter(config) {
|
|
|
931
1024
|
brand,
|
|
932
1025
|
copy,
|
|
933
1026
|
network,
|
|
1027
|
+
formation,
|
|
934
1028
|
crm,
|
|
935
1029
|
auth,
|
|
936
1030
|
pipeline,
|
|
@@ -947,6 +1041,147 @@ function defineChapter(config) {
|
|
|
947
1041
|
return chapter;
|
|
948
1042
|
}
|
|
949
1043
|
|
|
1044
|
+
// src/leader.ts
|
|
1045
|
+
import { composeCrmConfigs } from "@odla-ai/crm";
|
|
1046
|
+
function leaderCrmConfig(options = {}) {
|
|
1047
|
+
const base = options.base ?? defaultCrm("hub");
|
|
1048
|
+
const portfolio = {
|
|
1049
|
+
types: {
|
|
1050
|
+
chapter: {
|
|
1051
|
+
label: "Chapter",
|
|
1052
|
+
labelPlural: "Chapters",
|
|
1053
|
+
workspace: "portfolio",
|
|
1054
|
+
nameField: "name",
|
|
1055
|
+
fields: {
|
|
1056
|
+
name: { type: "string", label: "Name", required: true },
|
|
1057
|
+
slug: { type: "string", label: "Slug", slot: "s1" },
|
|
1058
|
+
region: { type: "string", label: "Region", slot: "s2" },
|
|
1059
|
+
url: { type: "url", label: "Website" },
|
|
1060
|
+
thesis: { type: "string", label: "Investment thesis" },
|
|
1061
|
+
launchedAt: { type: "date", label: "Launch date", slot: "d1" },
|
|
1062
|
+
notes: { type: "string", label: "Notes" }
|
|
1063
|
+
},
|
|
1064
|
+
pipeline: {
|
|
1065
|
+
stages: [
|
|
1066
|
+
{ id: "proposed", label: "Proposed" },
|
|
1067
|
+
{ id: "forming", label: "Forming" },
|
|
1068
|
+
{ id: "active", label: "Active" },
|
|
1069
|
+
{ id: "paused", label: "Paused" },
|
|
1070
|
+
{ id: "closed", label: "Closed", terminal: true }
|
|
1071
|
+
],
|
|
1072
|
+
transitions: {
|
|
1073
|
+
proposed: ["forming", "closed"],
|
|
1074
|
+
forming: ["active", "paused", "closed"],
|
|
1075
|
+
active: ["paused", "closed"],
|
|
1076
|
+
paused: ["forming", "active", "closed"]
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
facets: { rank: "manual" }
|
|
1080
|
+
},
|
|
1081
|
+
chapter_application: {
|
|
1082
|
+
label: "Chapter application",
|
|
1083
|
+
labelPlural: "Formation applications",
|
|
1084
|
+
workspace: "portfolio",
|
|
1085
|
+
nameField: "name",
|
|
1086
|
+
emailField: "email",
|
|
1087
|
+
fields: {
|
|
1088
|
+
name: { type: "string", label: "Applicant name", required: true },
|
|
1089
|
+
email: { type: "email", label: "Email", required: true },
|
|
1090
|
+
location: { type: "string", label: "Location", required: true, slot: "s1" },
|
|
1091
|
+
proposedChapter: { type: "string", label: "Proposed chapter", slot: "s2" },
|
|
1092
|
+
thesis: { type: "string", label: "Proposed thesis", required: true },
|
|
1093
|
+
experience: { type: "string", label: "Relevant experience" },
|
|
1094
|
+
collaborators: { type: "json", label: "Collaborators" },
|
|
1095
|
+
notes: { type: "string", label: "Internal notes" }
|
|
1096
|
+
},
|
|
1097
|
+
pipeline: {
|
|
1098
|
+
stages: [
|
|
1099
|
+
{ id: "submitted", label: "Submitted" },
|
|
1100
|
+
{ id: "reviewing", label: "Reviewing" },
|
|
1101
|
+
{ id: "interviewing", label: "Interviewing" },
|
|
1102
|
+
{ id: "approved", label: "Approved" },
|
|
1103
|
+
{ id: "forming", label: "Forming" },
|
|
1104
|
+
{ id: "declined", label: "Declined", terminal: true }
|
|
1105
|
+
],
|
|
1106
|
+
transitions: {
|
|
1107
|
+
submitted: ["reviewing", "declined"],
|
|
1108
|
+
reviewing: ["interviewing", "approved", "declined"],
|
|
1109
|
+
interviewing: ["approved", "declined"],
|
|
1110
|
+
approved: ["forming", "declined"]
|
|
1111
|
+
}
|
|
1112
|
+
},
|
|
1113
|
+
facets: { email: true, rank: "manual" }
|
|
1114
|
+
},
|
|
1115
|
+
deal: {
|
|
1116
|
+
label: "Deal",
|
|
1117
|
+
labelPlural: "Deal flow",
|
|
1118
|
+
workspace: "portfolio",
|
|
1119
|
+
nameField: "name",
|
|
1120
|
+
fields: {
|
|
1121
|
+
name: { type: "string", label: "Company or deal", required: true },
|
|
1122
|
+
chapter: { type: "string", label: "Source chapter", slot: "s1" },
|
|
1123
|
+
sector: { type: "string", label: "Sector", slot: "s2" },
|
|
1124
|
+
source: { type: "string", label: "Source", slot: "s3" },
|
|
1125
|
+
website: { type: "url", label: "Website" },
|
|
1126
|
+
amount: { type: "number", label: "Target amount", slot: "n1" },
|
|
1127
|
+
notes: { type: "string", label: "Notes" }
|
|
1128
|
+
},
|
|
1129
|
+
pipeline: {
|
|
1130
|
+
stages: [
|
|
1131
|
+
{ id: "sourced", label: "Sourced" },
|
|
1132
|
+
{ id: "screening", label: "Screening" },
|
|
1133
|
+
{ id: "diligence", label: "Diligence" },
|
|
1134
|
+
{ id: "committee", label: "Committee" },
|
|
1135
|
+
{ id: "offered", label: "Offered" },
|
|
1136
|
+
{ id: "invested", label: "Invested", terminal: true },
|
|
1137
|
+
{ id: "passed", label: "Passed", terminal: true }
|
|
1138
|
+
],
|
|
1139
|
+
transitions: {
|
|
1140
|
+
sourced: ["screening", "passed"],
|
|
1141
|
+
screening: ["diligence", "committee", "passed"],
|
|
1142
|
+
diligence: ["committee", "offered", "passed"],
|
|
1143
|
+
committee: ["offered", "passed"],
|
|
1144
|
+
offered: ["invested", "passed"],
|
|
1145
|
+
passed: ["sourced"]
|
|
1146
|
+
}
|
|
1147
|
+
},
|
|
1148
|
+
facets: { rank: "manual" }
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
relations: {
|
|
1152
|
+
formation_result: {
|
|
1153
|
+
from: "chapter_application",
|
|
1154
|
+
to: "chapter",
|
|
1155
|
+
label: "formed as",
|
|
1156
|
+
reverseLabel: "formation application"
|
|
1157
|
+
},
|
|
1158
|
+
chapter_deal: {
|
|
1159
|
+
from: "deal",
|
|
1160
|
+
to: "chapter",
|
|
1161
|
+
label: "sourced by",
|
|
1162
|
+
reverseLabel: "deal flow"
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
if (base.types.company) {
|
|
1167
|
+
portfolio.relations.deal_company = {
|
|
1168
|
+
from: "deal",
|
|
1169
|
+
to: "company",
|
|
1170
|
+
label: "concerns",
|
|
1171
|
+
reverseLabel: "deals"
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
if (base.types.person) {
|
|
1175
|
+
portfolio.relations.application_contact = {
|
|
1176
|
+
from: "person",
|
|
1177
|
+
to: "chapter_application",
|
|
1178
|
+
label: "submitted",
|
|
1179
|
+
reverseLabel: "applicant"
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
return composeCrmConfigs(base, portfolio);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
950
1185
|
// src/descriptor.ts
|
|
951
1186
|
import { createCrmIntegration } from "@odla-ai/crm";
|
|
952
1187
|
function createChapterIntegration(chapter, options = {}) {
|
|
@@ -985,7 +1220,15 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
985
1220
|
},
|
|
986
1221
|
rules: { ...crmDesc.rules, ...chapter.rules },
|
|
987
1222
|
seeds,
|
|
988
|
-
probes: [
|
|
1223
|
+
probes: [
|
|
1224
|
+
...crmDesc.probes ?? [],
|
|
1225
|
+
// The snapshot route is deliberately private. A credential-free smoke
|
|
1226
|
+
// request must prove that the route is mounted and closed.
|
|
1227
|
+
{ path: "/api/network/snapshot", expectedStatus: 401 },
|
|
1228
|
+
// Formation metadata is public only when the host explicitly enables
|
|
1229
|
+
// the bounded intake contract.
|
|
1230
|
+
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : []
|
|
1231
|
+
],
|
|
989
1232
|
runbooks
|
|
990
1233
|
};
|
|
991
1234
|
}
|
|
@@ -1189,11 +1432,20 @@ function canceledPatch() {
|
|
|
1189
1432
|
}
|
|
1190
1433
|
|
|
1191
1434
|
// src/network.ts
|
|
1192
|
-
import {
|
|
1435
|
+
import {
|
|
1436
|
+
createRecord,
|
|
1437
|
+
getRecordByOrigin,
|
|
1438
|
+
updateRecord,
|
|
1439
|
+
upsertRecordOrigin
|
|
1440
|
+
} from "@odla-ai/crm";
|
|
1441
|
+
|
|
1442
|
+
// src/network-contract.ts
|
|
1193
1443
|
var DEFAULT_SHARE_FIELDS = {
|
|
1194
1444
|
person: ["name", "email", "firstName", "lastName", "phone", "linkedin"],
|
|
1195
1445
|
company: ["name", "domain", "industry", "location", "linkedin", "notes"]
|
|
1196
1446
|
};
|
|
1447
|
+
|
|
1448
|
+
// src/network.ts
|
|
1197
1449
|
function sharedPersonInput(person) {
|
|
1198
1450
|
const email = person.email.toLowerCase();
|
|
1199
1451
|
const fullName = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
|
|
@@ -1215,25 +1467,55 @@ function shortHash(value) {
|
|
|
1215
1467
|
}
|
|
1216
1468
|
return `${(a >>> 0).toString(36)}${(b >>> 0).toString(36)}`;
|
|
1217
1469
|
}
|
|
1218
|
-
function networkSourceTag(type, hubRecordId) {
|
|
1470
|
+
function networkSourceTag(type, hubRecordId, sourceId) {
|
|
1219
1471
|
const typeKey = type.toLowerCase();
|
|
1220
1472
|
const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
|
|
1221
|
-
const
|
|
1473
|
+
const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
|
|
1474
|
+
const raw = `${prefix}:${hubRecordId}`;
|
|
1222
1475
|
if (readable && raw.length <= 64) return raw;
|
|
1223
|
-
return `network:${typeKey.slice(0,
|
|
1224
|
-
}
|
|
1225
|
-
function normalizeSharedRecord(record) {
|
|
1226
|
-
if ("input" in record
|
|
1476
|
+
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
1477
|
+
}
|
|
1478
|
+
function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
|
|
1479
|
+
if ("input" in record && record.version === 2) {
|
|
1480
|
+
return {
|
|
1481
|
+
version: 2,
|
|
1482
|
+
sourceId: record.source.siteId,
|
|
1483
|
+
sourceRecordId: record.source.recordId,
|
|
1484
|
+
type: record.type,
|
|
1485
|
+
input: record.input
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
if ("input" in record) {
|
|
1489
|
+
return {
|
|
1490
|
+
version: 1,
|
|
1491
|
+
sourceId: fallbackSourceId,
|
|
1492
|
+
sourceRecordId: record.hubRecordId,
|
|
1493
|
+
type: record.type,
|
|
1494
|
+
input: record.input
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1227
1497
|
if ("type" in record && record.type === "company") {
|
|
1228
1498
|
const input = { name: record.name };
|
|
1229
1499
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
1230
1500
|
if (record[key]) input[key] = record[key];
|
|
1231
1501
|
}
|
|
1232
|
-
return {
|
|
1502
|
+
return {
|
|
1503
|
+
version: 1,
|
|
1504
|
+
sourceId: fallbackSourceId,
|
|
1505
|
+
sourceRecordId: record.hubRecordId,
|
|
1506
|
+
type: "company",
|
|
1507
|
+
input
|
|
1508
|
+
};
|
|
1233
1509
|
}
|
|
1234
|
-
return {
|
|
1510
|
+
return {
|
|
1511
|
+
version: 1,
|
|
1512
|
+
sourceId: fallbackSourceId,
|
|
1513
|
+
sourceRecordId: record.hubRecordId,
|
|
1514
|
+
type: "person",
|
|
1515
|
+
input: sharedPersonInput(record)
|
|
1516
|
+
};
|
|
1235
1517
|
}
|
|
1236
|
-
function sharedRecordFromCrm(crm, record, target) {
|
|
1518
|
+
function sharedRecordFromCrm(crm, record, target, sourceId) {
|
|
1237
1519
|
if (target.fields && !target.fields[record.type]) {
|
|
1238
1520
|
throw new Error(`${target.name} does not accept "${record.type}" records`);
|
|
1239
1521
|
}
|
|
@@ -1249,7 +1531,7 @@ function sharedRecordFromCrm(crm, record, target) {
|
|
|
1249
1531
|
if (value !== void 0) input[field] = value;
|
|
1250
1532
|
}
|
|
1251
1533
|
if (input[nameField] === void 0) input[nameField] = record.name;
|
|
1252
|
-
return { version: 1, type: record.type, hubRecordId: record.id, input };
|
|
1534
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record.id }, type: record.type, input } : { version: 1, type: record.type, hubRecordId: record.id, input };
|
|
1253
1535
|
}
|
|
1254
1536
|
async function upsertPerson(deps, opts) {
|
|
1255
1537
|
const email = opts.email.toLowerCase();
|
|
@@ -1264,6 +1546,12 @@ async function upsertPerson(deps, opts) {
|
|
|
1264
1546
|
return { recordId: created.id };
|
|
1265
1547
|
}
|
|
1266
1548
|
async function findSharedRecord(deps, record, tag) {
|
|
1549
|
+
const structured = await getRecordByOrigin(
|
|
1550
|
+
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
1551
|
+
record.sourceId,
|
|
1552
|
+
record.sourceRecordId
|
|
1553
|
+
);
|
|
1554
|
+
if (structured) return structured;
|
|
1267
1555
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
1268
1556
|
const mappedId = mapped.crm_tag?.[0]?.recordId;
|
|
1269
1557
|
if (typeof mappedId === "string") {
|
|
@@ -1297,12 +1585,15 @@ async function findSharedRecord(deps, record, tag) {
|
|
|
1297
1585
|
}
|
|
1298
1586
|
return void 0;
|
|
1299
1587
|
}
|
|
1300
|
-
async function projectSharedRecord(deps, shared) {
|
|
1301
|
-
const record = normalizeSharedRecord(shared);
|
|
1302
|
-
if (!record.type.trim() || !record.
|
|
1303
|
-
throw new Error("type and
|
|
1588
|
+
async function projectSharedRecord(deps, shared, options = {}) {
|
|
1589
|
+
const record = normalizeSharedRecord(shared, options.sourceId);
|
|
1590
|
+
if (!record.type.trim() || !record.sourceId.trim() || !record.sourceRecordId.trim()) {
|
|
1591
|
+
throw new Error("type, source site id, and source record id must be non-empty");
|
|
1304
1592
|
}
|
|
1305
|
-
|
|
1593
|
+
if (options.sourceId && options.sourceId !== record.sourceId) {
|
|
1594
|
+
throw new Error("signed sender does not match payload source");
|
|
1595
|
+
}
|
|
1596
|
+
const tag = networkSourceTag(record.type, record.sourceRecordId, record.version === 2 ? record.sourceId : void 0);
|
|
1306
1597
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1307
1598
|
const existing = await findSharedRecord(deps, record, tag);
|
|
1308
1599
|
let recordId;
|
|
@@ -1310,7 +1601,7 @@ async function projectSharedRecord(deps, shared) {
|
|
|
1310
1601
|
await updateRecord(crmDeps, { id: existing.id, input: record.input });
|
|
1311
1602
|
recordId = existing.id;
|
|
1312
1603
|
} else {
|
|
1313
|
-
recordId = `network_${shortHash(`${record.type}\0${record.
|
|
1604
|
+
recordId = `network_${shortHash(`${record.sourceId}\0${record.type}\0${record.sourceRecordId}`)}`;
|
|
1314
1605
|
await createRecord({ ...crmDeps, newId: () => recordId }, {
|
|
1315
1606
|
type: record.type,
|
|
1316
1607
|
input: record.input,
|
|
@@ -1321,6 +1612,13 @@ async function projectSharedRecord(deps, shared) {
|
|
|
1321
1612
|
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
|
|
1322
1613
|
{ mutationId: `share-map:${tag}:${recordId}` }
|
|
1323
1614
|
);
|
|
1615
|
+
await upsertRecordOrigin(crmDeps, {
|
|
1616
|
+
recordId,
|
|
1617
|
+
sourceId: record.sourceId,
|
|
1618
|
+
sourceRecordId: record.sourceRecordId,
|
|
1619
|
+
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
1620
|
+
payloadVersion: record.version
|
|
1621
|
+
});
|
|
1324
1622
|
return { recordId };
|
|
1325
1623
|
}
|
|
1326
1624
|
async function projectApplicant(deps, applicant) {
|
|
@@ -2006,6 +2304,7 @@ export {
|
|
|
2006
2304
|
findApplicationRef,
|
|
2007
2305
|
firstPaymentPatch,
|
|
2008
2306
|
formatChapterCopy,
|
|
2307
|
+
formationFields,
|
|
2009
2308
|
getVaultSecret,
|
|
2010
2309
|
hasDisclaimerAck,
|
|
2011
2310
|
introIdempotencyKey,
|
|
@@ -2015,6 +2314,7 @@ export {
|
|
|
2015
2314
|
isSlotAvailable,
|
|
2016
2315
|
isValidEmail,
|
|
2017
2316
|
joinConfig,
|
|
2317
|
+
leaderCrmConfig,
|
|
2018
2318
|
meetingCreateRow,
|
|
2019
2319
|
meetingRescheduleUpdate,
|
|
2020
2320
|
memberApplication,
|
|
@@ -2036,6 +2336,7 @@ export {
|
|
|
2036
2336
|
resolveApplication,
|
|
2037
2337
|
resolveAuth,
|
|
2038
2338
|
resolveChapterCopy,
|
|
2339
|
+
resolveLeaderFormation,
|
|
2039
2340
|
resolvePipeline,
|
|
2040
2341
|
resolveScheduling,
|
|
2041
2342
|
roleFromClaim,
|