@odla-ai/chapter 0.25.8 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -15
- 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-Vof2eTMt.d.ts} +78 -42
- package/dist/index.cjs +335 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +175 -58
- package/dist/index.d.ts +175 -58
- package/dist/index.js +329 -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 +552 -216
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +78 -42
- package/dist/worker/index.d.ts +78 -42
- package/dist/worker/index.js +557 -216
- 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,9 +805,85 @@ 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_]*$/;
|
|
886
|
+
var BINDING = /^[A-Z][A-Z0-9_]{0,127}$/;
|
|
794
887
|
function resolveNetwork(config, crm) {
|
|
795
888
|
const seen = /* @__PURE__ */ new Set();
|
|
796
889
|
const targets = [];
|
|
@@ -836,10 +929,17 @@ function resolveNetwork(config, crm) {
|
|
|
836
929
|
if (!/^[a-zA-Z][a-zA-Z0-9_-]{1,127}$/.test(secretName)) {
|
|
837
930
|
throw new Error(`defineChapter.network.targets.${target.id}.secretName: must be a vault-key identifier`);
|
|
838
931
|
}
|
|
932
|
+
const binding = target.binding?.trim();
|
|
933
|
+
if (binding && !BINDING.test(binding)) {
|
|
934
|
+
throw new Error(
|
|
935
|
+
`defineChapter.network.targets.${target.id}.binding: must be an uppercase Worker binding identifier`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
839
938
|
targets.push({
|
|
840
939
|
id: target.id,
|
|
841
940
|
name: target.name?.trim() || target.id,
|
|
842
941
|
url: url.origin,
|
|
942
|
+
...binding ? { binding } : {},
|
|
843
943
|
secretName,
|
|
844
944
|
...fields ? { fields } : {}
|
|
845
945
|
});
|
|
@@ -876,6 +976,7 @@ function defineChapter(config) {
|
|
|
876
976
|
const brand = { ...config.brand, wordmark: config.brand?.wordmark ?? name };
|
|
877
977
|
const copy = resolveChapterCopy(config.copy);
|
|
878
978
|
const network = resolveNetwork(config, crm);
|
|
979
|
+
const formation = resolveLeaderFormation(config.formation, crm);
|
|
879
980
|
const { schema, rules } = chapterDb(mode, auth);
|
|
880
981
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
881
982
|
const account = config.account ?? "none";
|
|
@@ -931,6 +1032,7 @@ function defineChapter(config) {
|
|
|
931
1032
|
brand,
|
|
932
1033
|
copy,
|
|
933
1034
|
network,
|
|
1035
|
+
formation,
|
|
934
1036
|
crm,
|
|
935
1037
|
auth,
|
|
936
1038
|
pipeline,
|
|
@@ -947,6 +1049,147 @@ function defineChapter(config) {
|
|
|
947
1049
|
return chapter;
|
|
948
1050
|
}
|
|
949
1051
|
|
|
1052
|
+
// src/leader.ts
|
|
1053
|
+
import { composeCrmConfigs } from "@odla-ai/crm";
|
|
1054
|
+
function leaderCrmConfig(options = {}) {
|
|
1055
|
+
const base = options.base ?? defaultCrm("hub");
|
|
1056
|
+
const portfolio = {
|
|
1057
|
+
types: {
|
|
1058
|
+
chapter: {
|
|
1059
|
+
label: "Chapter",
|
|
1060
|
+
labelPlural: "Chapters",
|
|
1061
|
+
workspace: "portfolio",
|
|
1062
|
+
nameField: "name",
|
|
1063
|
+
fields: {
|
|
1064
|
+
name: { type: "string", label: "Name", required: true },
|
|
1065
|
+
slug: { type: "string", label: "Slug", slot: "s1" },
|
|
1066
|
+
region: { type: "string", label: "Region", slot: "s2" },
|
|
1067
|
+
url: { type: "url", label: "Website" },
|
|
1068
|
+
thesis: { type: "string", label: "Investment thesis" },
|
|
1069
|
+
launchedAt: { type: "date", label: "Launch date", slot: "d1" },
|
|
1070
|
+
notes: { type: "string", label: "Notes" }
|
|
1071
|
+
},
|
|
1072
|
+
pipeline: {
|
|
1073
|
+
stages: [
|
|
1074
|
+
{ id: "proposed", label: "Proposed" },
|
|
1075
|
+
{ id: "forming", label: "Forming" },
|
|
1076
|
+
{ id: "active", label: "Active" },
|
|
1077
|
+
{ id: "paused", label: "Paused" },
|
|
1078
|
+
{ id: "closed", label: "Closed", terminal: true }
|
|
1079
|
+
],
|
|
1080
|
+
transitions: {
|
|
1081
|
+
proposed: ["forming", "closed"],
|
|
1082
|
+
forming: ["active", "paused", "closed"],
|
|
1083
|
+
active: ["paused", "closed"],
|
|
1084
|
+
paused: ["forming", "active", "closed"]
|
|
1085
|
+
}
|
|
1086
|
+
},
|
|
1087
|
+
facets: { rank: "manual" }
|
|
1088
|
+
},
|
|
1089
|
+
chapter_application: {
|
|
1090
|
+
label: "Chapter application",
|
|
1091
|
+
labelPlural: "Formation applications",
|
|
1092
|
+
workspace: "portfolio",
|
|
1093
|
+
nameField: "name",
|
|
1094
|
+
emailField: "email",
|
|
1095
|
+
fields: {
|
|
1096
|
+
name: { type: "string", label: "Applicant name", required: true },
|
|
1097
|
+
email: { type: "email", label: "Email", required: true },
|
|
1098
|
+
location: { type: "string", label: "Location", required: true, slot: "s1" },
|
|
1099
|
+
proposedChapter: { type: "string", label: "Proposed chapter", slot: "s2" },
|
|
1100
|
+
thesis: { type: "string", label: "Proposed thesis", required: true },
|
|
1101
|
+
experience: { type: "string", label: "Relevant experience" },
|
|
1102
|
+
collaborators: { type: "json", label: "Collaborators" },
|
|
1103
|
+
notes: { type: "string", label: "Internal notes" }
|
|
1104
|
+
},
|
|
1105
|
+
pipeline: {
|
|
1106
|
+
stages: [
|
|
1107
|
+
{ id: "submitted", label: "Submitted" },
|
|
1108
|
+
{ id: "reviewing", label: "Reviewing" },
|
|
1109
|
+
{ id: "interviewing", label: "Interviewing" },
|
|
1110
|
+
{ id: "approved", label: "Approved" },
|
|
1111
|
+
{ id: "forming", label: "Forming" },
|
|
1112
|
+
{ id: "declined", label: "Declined", terminal: true }
|
|
1113
|
+
],
|
|
1114
|
+
transitions: {
|
|
1115
|
+
submitted: ["reviewing", "declined"],
|
|
1116
|
+
reviewing: ["interviewing", "approved", "declined"],
|
|
1117
|
+
interviewing: ["approved", "declined"],
|
|
1118
|
+
approved: ["forming", "declined"]
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
facets: { email: true, rank: "manual" }
|
|
1122
|
+
},
|
|
1123
|
+
deal: {
|
|
1124
|
+
label: "Deal",
|
|
1125
|
+
labelPlural: "Deal flow",
|
|
1126
|
+
workspace: "portfolio",
|
|
1127
|
+
nameField: "name",
|
|
1128
|
+
fields: {
|
|
1129
|
+
name: { type: "string", label: "Company or deal", required: true },
|
|
1130
|
+
chapter: { type: "string", label: "Source chapter", slot: "s1" },
|
|
1131
|
+
sector: { type: "string", label: "Sector", slot: "s2" },
|
|
1132
|
+
source: { type: "string", label: "Source", slot: "s3" },
|
|
1133
|
+
website: { type: "url", label: "Website" },
|
|
1134
|
+
amount: { type: "number", label: "Target amount", slot: "n1" },
|
|
1135
|
+
notes: { type: "string", label: "Notes" }
|
|
1136
|
+
},
|
|
1137
|
+
pipeline: {
|
|
1138
|
+
stages: [
|
|
1139
|
+
{ id: "sourced", label: "Sourced" },
|
|
1140
|
+
{ id: "screening", label: "Screening" },
|
|
1141
|
+
{ id: "diligence", label: "Diligence" },
|
|
1142
|
+
{ id: "committee", label: "Committee" },
|
|
1143
|
+
{ id: "offered", label: "Offered" },
|
|
1144
|
+
{ id: "invested", label: "Invested", terminal: true },
|
|
1145
|
+
{ id: "passed", label: "Passed", terminal: true }
|
|
1146
|
+
],
|
|
1147
|
+
transitions: {
|
|
1148
|
+
sourced: ["screening", "passed"],
|
|
1149
|
+
screening: ["diligence", "committee", "passed"],
|
|
1150
|
+
diligence: ["committee", "offered", "passed"],
|
|
1151
|
+
committee: ["offered", "passed"],
|
|
1152
|
+
offered: ["invested", "passed"],
|
|
1153
|
+
passed: ["sourced"]
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
facets: { rank: "manual" }
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
relations: {
|
|
1160
|
+
formation_result: {
|
|
1161
|
+
from: "chapter_application",
|
|
1162
|
+
to: "chapter",
|
|
1163
|
+
label: "formed as",
|
|
1164
|
+
reverseLabel: "formation application"
|
|
1165
|
+
},
|
|
1166
|
+
chapter_deal: {
|
|
1167
|
+
from: "deal",
|
|
1168
|
+
to: "chapter",
|
|
1169
|
+
label: "sourced by",
|
|
1170
|
+
reverseLabel: "deal flow"
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
if (base.types.company) {
|
|
1175
|
+
portfolio.relations.deal_company = {
|
|
1176
|
+
from: "deal",
|
|
1177
|
+
to: "company",
|
|
1178
|
+
label: "concerns",
|
|
1179
|
+
reverseLabel: "deals"
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
if (base.types.person) {
|
|
1183
|
+
portfolio.relations.application_contact = {
|
|
1184
|
+
from: "person",
|
|
1185
|
+
to: "chapter_application",
|
|
1186
|
+
label: "submitted",
|
|
1187
|
+
reverseLabel: "applicant"
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
return composeCrmConfigs(base, portfolio);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
950
1193
|
// src/descriptor.ts
|
|
951
1194
|
import { createCrmIntegration } from "@odla-ai/crm";
|
|
952
1195
|
function createChapterIntegration(chapter, options = {}) {
|
|
@@ -985,7 +1228,15 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
985
1228
|
},
|
|
986
1229
|
rules: { ...crmDesc.rules, ...chapter.rules },
|
|
987
1230
|
seeds,
|
|
988
|
-
probes: [
|
|
1231
|
+
probes: [
|
|
1232
|
+
...crmDesc.probes ?? [],
|
|
1233
|
+
// The snapshot route is deliberately private. A credential-free smoke
|
|
1234
|
+
// request must prove that the route is mounted and closed.
|
|
1235
|
+
{ path: "/api/network/snapshot", expectedStatus: 401 },
|
|
1236
|
+
// Formation metadata is public only when the host explicitly enables
|
|
1237
|
+
// the bounded intake contract.
|
|
1238
|
+
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : []
|
|
1239
|
+
],
|
|
989
1240
|
runbooks
|
|
990
1241
|
};
|
|
991
1242
|
}
|
|
@@ -1189,11 +1440,20 @@ function canceledPatch() {
|
|
|
1189
1440
|
}
|
|
1190
1441
|
|
|
1191
1442
|
// src/network.ts
|
|
1192
|
-
import {
|
|
1443
|
+
import {
|
|
1444
|
+
createRecord,
|
|
1445
|
+
getRecordByOrigin,
|
|
1446
|
+
updateRecord,
|
|
1447
|
+
upsertRecordOrigin
|
|
1448
|
+
} from "@odla-ai/crm";
|
|
1449
|
+
|
|
1450
|
+
// src/network-contract.ts
|
|
1193
1451
|
var DEFAULT_SHARE_FIELDS = {
|
|
1194
1452
|
person: ["name", "email", "firstName", "lastName", "phone", "linkedin"],
|
|
1195
1453
|
company: ["name", "domain", "industry", "location", "linkedin", "notes"]
|
|
1196
1454
|
};
|
|
1455
|
+
|
|
1456
|
+
// src/network.ts
|
|
1197
1457
|
function sharedPersonInput(person) {
|
|
1198
1458
|
const email = person.email.toLowerCase();
|
|
1199
1459
|
const fullName = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
|
|
@@ -1215,25 +1475,55 @@ function shortHash(value) {
|
|
|
1215
1475
|
}
|
|
1216
1476
|
return `${(a >>> 0).toString(36)}${(b >>> 0).toString(36)}`;
|
|
1217
1477
|
}
|
|
1218
|
-
function networkSourceTag(type, hubRecordId) {
|
|
1478
|
+
function networkSourceTag(type, hubRecordId, sourceId) {
|
|
1219
1479
|
const typeKey = type.toLowerCase();
|
|
1220
1480
|
const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
|
|
1221
|
-
const
|
|
1481
|
+
const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
|
|
1482
|
+
const raw = `${prefix}:${hubRecordId}`;
|
|
1222
1483
|
if (readable && raw.length <= 64) return raw;
|
|
1223
|
-
return `network:${typeKey.slice(0,
|
|
1224
|
-
}
|
|
1225
|
-
function normalizeSharedRecord(record) {
|
|
1226
|
-
if ("input" in record
|
|
1484
|
+
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
1485
|
+
}
|
|
1486
|
+
function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
|
|
1487
|
+
if ("input" in record && record.version === 2) {
|
|
1488
|
+
return {
|
|
1489
|
+
version: 2,
|
|
1490
|
+
sourceId: record.source.siteId,
|
|
1491
|
+
sourceRecordId: record.source.recordId,
|
|
1492
|
+
type: record.type,
|
|
1493
|
+
input: record.input
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
if ("input" in record) {
|
|
1497
|
+
return {
|
|
1498
|
+
version: 1,
|
|
1499
|
+
sourceId: fallbackSourceId,
|
|
1500
|
+
sourceRecordId: record.hubRecordId,
|
|
1501
|
+
type: record.type,
|
|
1502
|
+
input: record.input
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1227
1505
|
if ("type" in record && record.type === "company") {
|
|
1228
1506
|
const input = { name: record.name };
|
|
1229
1507
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
1230
1508
|
if (record[key]) input[key] = record[key];
|
|
1231
1509
|
}
|
|
1232
|
-
return {
|
|
1510
|
+
return {
|
|
1511
|
+
version: 1,
|
|
1512
|
+
sourceId: fallbackSourceId,
|
|
1513
|
+
sourceRecordId: record.hubRecordId,
|
|
1514
|
+
type: "company",
|
|
1515
|
+
input
|
|
1516
|
+
};
|
|
1233
1517
|
}
|
|
1234
|
-
return {
|
|
1518
|
+
return {
|
|
1519
|
+
version: 1,
|
|
1520
|
+
sourceId: fallbackSourceId,
|
|
1521
|
+
sourceRecordId: record.hubRecordId,
|
|
1522
|
+
type: "person",
|
|
1523
|
+
input: sharedPersonInput(record)
|
|
1524
|
+
};
|
|
1235
1525
|
}
|
|
1236
|
-
function sharedRecordFromCrm(crm, record, target) {
|
|
1526
|
+
function sharedRecordFromCrm(crm, record, target, sourceId) {
|
|
1237
1527
|
if (target.fields && !target.fields[record.type]) {
|
|
1238
1528
|
throw new Error(`${target.name} does not accept "${record.type}" records`);
|
|
1239
1529
|
}
|
|
@@ -1249,7 +1539,7 @@ function sharedRecordFromCrm(crm, record, target) {
|
|
|
1249
1539
|
if (value !== void 0) input[field] = value;
|
|
1250
1540
|
}
|
|
1251
1541
|
if (input[nameField] === void 0) input[nameField] = record.name;
|
|
1252
|
-
return { version: 1, type: record.type, hubRecordId: record.id, input };
|
|
1542
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record.id }, type: record.type, input } : { version: 1, type: record.type, hubRecordId: record.id, input };
|
|
1253
1543
|
}
|
|
1254
1544
|
async function upsertPerson(deps, opts) {
|
|
1255
1545
|
const email = opts.email.toLowerCase();
|
|
@@ -1264,6 +1554,12 @@ async function upsertPerson(deps, opts) {
|
|
|
1264
1554
|
return { recordId: created.id };
|
|
1265
1555
|
}
|
|
1266
1556
|
async function findSharedRecord(deps, record, tag) {
|
|
1557
|
+
const structured = await getRecordByOrigin(
|
|
1558
|
+
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
1559
|
+
record.sourceId,
|
|
1560
|
+
record.sourceRecordId
|
|
1561
|
+
);
|
|
1562
|
+
if (structured) return structured;
|
|
1267
1563
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
1268
1564
|
const mappedId = mapped.crm_tag?.[0]?.recordId;
|
|
1269
1565
|
if (typeof mappedId === "string") {
|
|
@@ -1297,12 +1593,15 @@ async function findSharedRecord(deps, record, tag) {
|
|
|
1297
1593
|
}
|
|
1298
1594
|
return void 0;
|
|
1299
1595
|
}
|
|
1300
|
-
async function projectSharedRecord(deps, shared) {
|
|
1301
|
-
const record = normalizeSharedRecord(shared);
|
|
1302
|
-
if (!record.type.trim() || !record.
|
|
1303
|
-
throw new Error("type and
|
|
1596
|
+
async function projectSharedRecord(deps, shared, options = {}) {
|
|
1597
|
+
const record = normalizeSharedRecord(shared, options.sourceId);
|
|
1598
|
+
if (!record.type.trim() || !record.sourceId.trim() || !record.sourceRecordId.trim()) {
|
|
1599
|
+
throw new Error("type, source site id, and source record id must be non-empty");
|
|
1304
1600
|
}
|
|
1305
|
-
|
|
1601
|
+
if (options.sourceId && options.sourceId !== record.sourceId) {
|
|
1602
|
+
throw new Error("signed sender does not match payload source");
|
|
1603
|
+
}
|
|
1604
|
+
const tag = networkSourceTag(record.type, record.sourceRecordId, record.version === 2 ? record.sourceId : void 0);
|
|
1306
1605
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1307
1606
|
const existing = await findSharedRecord(deps, record, tag);
|
|
1308
1607
|
let recordId;
|
|
@@ -1310,7 +1609,7 @@ async function projectSharedRecord(deps, shared) {
|
|
|
1310
1609
|
await updateRecord(crmDeps, { id: existing.id, input: record.input });
|
|
1311
1610
|
recordId = existing.id;
|
|
1312
1611
|
} else {
|
|
1313
|
-
recordId = `network_${shortHash(`${record.type}\0${record.
|
|
1612
|
+
recordId = `network_${shortHash(`${record.sourceId}\0${record.type}\0${record.sourceRecordId}`)}`;
|
|
1314
1613
|
await createRecord({ ...crmDeps, newId: () => recordId }, {
|
|
1315
1614
|
type: record.type,
|
|
1316
1615
|
input: record.input,
|
|
@@ -1321,6 +1620,13 @@ async function projectSharedRecord(deps, shared) {
|
|
|
1321
1620
|
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
|
|
1322
1621
|
{ mutationId: `share-map:${tag}:${recordId}` }
|
|
1323
1622
|
);
|
|
1623
|
+
await upsertRecordOrigin(crmDeps, {
|
|
1624
|
+
recordId,
|
|
1625
|
+
sourceId: record.sourceId,
|
|
1626
|
+
sourceRecordId: record.sourceRecordId,
|
|
1627
|
+
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
1628
|
+
payloadVersion: record.version
|
|
1629
|
+
});
|
|
1324
1630
|
return { recordId };
|
|
1325
1631
|
}
|
|
1326
1632
|
async function projectApplicant(deps, applicant) {
|
|
@@ -2006,6 +2312,7 @@ export {
|
|
|
2006
2312
|
findApplicationRef,
|
|
2007
2313
|
firstPaymentPatch,
|
|
2008
2314
|
formatChapterCopy,
|
|
2315
|
+
formationFields,
|
|
2009
2316
|
getVaultSecret,
|
|
2010
2317
|
hasDisclaimerAck,
|
|
2011
2318
|
introIdempotencyKey,
|
|
@@ -2015,6 +2322,7 @@ export {
|
|
|
2015
2322
|
isSlotAvailable,
|
|
2016
2323
|
isValidEmail,
|
|
2017
2324
|
joinConfig,
|
|
2325
|
+
leaderCrmConfig,
|
|
2018
2326
|
meetingCreateRow,
|
|
2019
2327
|
meetingRescheduleUpdate,
|
|
2020
2328
|
memberApplication,
|
|
@@ -2036,6 +2344,7 @@ export {
|
|
|
2036
2344
|
resolveApplication,
|
|
2037
2345
|
resolveAuth,
|
|
2038
2346
|
resolveChapterCopy,
|
|
2347
|
+
resolveLeaderFormation,
|
|
2039
2348
|
resolvePipeline,
|
|
2040
2349
|
resolveScheduling,
|
|
2041
2350
|
roleFromClaim,
|