@odla-ai/chapter 0.25.6 → 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-5SWVMJZN.js → chunk-4QZEAWQL.js} +183 -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 +552 -208
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +76 -42
- package/dist/worker/index.d.ts +76 -42
- package/dist/worker/index.js +558 -209
- package/dist/worker/index.js.map +1 -1
- package/package.json +3 -3
- package/runbooks/adopt-existing.md +12 -1
- package/runbooks/greenfield.md +29 -7
- package/dist/chunk-5SWVMJZN.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.cjs
CHANGED
|
@@ -57,6 +57,7 @@ __export(index_exports, {
|
|
|
57
57
|
findApplicationRef: () => findApplicationRef,
|
|
58
58
|
firstPaymentPatch: () => firstPaymentPatch,
|
|
59
59
|
formatChapterCopy: () => formatChapterCopy,
|
|
60
|
+
formationFields: () => formationFields,
|
|
60
61
|
getVaultSecret: () => getVaultSecret,
|
|
61
62
|
hasDisclaimerAck: () => hasDisclaimerAck,
|
|
62
63
|
introIdempotencyKey: () => introIdempotencyKey,
|
|
@@ -66,6 +67,7 @@ __export(index_exports, {
|
|
|
66
67
|
isSlotAvailable: () => isSlotAvailable,
|
|
67
68
|
isValidEmail: () => isValidEmail,
|
|
68
69
|
joinConfig: () => joinConfig,
|
|
70
|
+
leaderCrmConfig: () => leaderCrmConfig,
|
|
69
71
|
meetingCreateRow: () => meetingCreateRow,
|
|
70
72
|
meetingRescheduleUpdate: () => meetingRescheduleUpdate,
|
|
71
73
|
memberApplication: () => memberApplication,
|
|
@@ -87,6 +89,7 @@ __export(index_exports, {
|
|
|
87
89
|
resolveApplication: () => resolveApplication,
|
|
88
90
|
resolveAuth: () => resolveAuth,
|
|
89
91
|
resolveChapterCopy: () => resolveChapterCopy,
|
|
92
|
+
resolveLeaderFormation: () => resolveLeaderFormation,
|
|
90
93
|
resolvePipeline: () => resolvePipeline,
|
|
91
94
|
resolveScheduling: () => resolveScheduling,
|
|
92
95
|
roleFromClaim: () => roleFromClaim,
|
|
@@ -630,12 +633,17 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
630
633
|
overview: "Overview",
|
|
631
634
|
billing: "Billing",
|
|
632
635
|
people: "People",
|
|
636
|
+
portfolio: "Portfolio",
|
|
633
637
|
settings: "Settings",
|
|
634
638
|
calendar: "Calendar",
|
|
635
639
|
email: "Email",
|
|
640
|
+
chapters: "Chapters",
|
|
641
|
+
formations: "Formation applications",
|
|
642
|
+
dealFlow: "Deal flow",
|
|
636
643
|
dashboardViewsLabel: "Dashboard views",
|
|
637
644
|
settingsViewsLabel: "Settings views",
|
|
638
|
-
collectionsLabel: "CRM collections"
|
|
645
|
+
collectionsLabel: "CRM collections",
|
|
646
|
+
portfolioViewsLabel: "Portfolio views"
|
|
639
647
|
},
|
|
640
648
|
dashboard: {
|
|
641
649
|
loading: "Loading dashboard\u2026",
|
|
@@ -741,7 +749,19 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
741
749
|
shareWith: "Share with {target}",
|
|
742
750
|
shared: "Shared with {target}.",
|
|
743
751
|
shareFailed: "Could not share with {target}.",
|
|
744
|
-
deliveryFailed: "Delivery failed."
|
|
752
|
+
deliveryFailed: "Delivery failed.",
|
|
753
|
+
loadingRollup: "Loading network overview\u2026",
|
|
754
|
+
loadRollupFailed: "Couldn't load the network overview",
|
|
755
|
+
overview: "Network overview",
|
|
756
|
+
configuredFollowers: "Configured followers",
|
|
757
|
+
reachableFollowers: "Reachable followers",
|
|
758
|
+
recordsAcrossFollowers: "Records across followers",
|
|
759
|
+
noFollowers: "No follower sites are configured.",
|
|
760
|
+
followers: "Followers",
|
|
761
|
+
available: "available",
|
|
762
|
+
unavailable: "unavailable",
|
|
763
|
+
recordTotals: "Portfolio records",
|
|
764
|
+
noRecordTotals: "No aggregate records are available yet."
|
|
745
765
|
},
|
|
746
766
|
records: {
|
|
747
767
|
workflowMissing: "This record is not linked to an application workflow.",
|
|
@@ -899,6 +919,81 @@ function formatChapterCopy(template, values) {
|
|
|
899
919
|
);
|
|
900
920
|
}
|
|
901
921
|
|
|
922
|
+
// src/formation.ts
|
|
923
|
+
var DEFAULT_REQUIRED2 = ["name", "email", "location", "thesis"];
|
|
924
|
+
var DEFAULT_OPTIONAL2 = ["proposedChapter", "experience", "collaborators"];
|
|
925
|
+
function resolveLeaderFormation(input, crm) {
|
|
926
|
+
const type = input?.type ?? "chapter_application";
|
|
927
|
+
if (!input) {
|
|
928
|
+
return {
|
|
929
|
+
enabled: false,
|
|
930
|
+
type,
|
|
931
|
+
required: [],
|
|
932
|
+
optional: [],
|
|
933
|
+
maxLen: {},
|
|
934
|
+
defaultMaxLen: 4e3,
|
|
935
|
+
bodyCap: 32768
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
const def = crm.config.types[type];
|
|
939
|
+
if (!def) throw new Error(`defineChapter.formation.type: unknown CRM type "${type}"`);
|
|
940
|
+
const required = input.required ?? (type === "chapter_application" ? DEFAULT_REQUIRED2 : []);
|
|
941
|
+
const optional = input.optional ?? (type === "chapter_application" ? DEFAULT_OPTIONAL2 : []);
|
|
942
|
+
for (const [name, values] of [["required", required], ["optional", optional]]) {
|
|
943
|
+
if (!Array.isArray(values) || !values.every((field) => typeof field === "string" && field !== "")) {
|
|
944
|
+
throw new Error(`defineChapter.formation.${name}: must be an array of field-name strings`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const duplicates = [...required, ...optional].filter((field, index, fields) => fields.indexOf(field) !== index);
|
|
948
|
+
if (duplicates.length) throw new Error(`defineChapter.formation: duplicate public field "${duplicates[0]}"`);
|
|
949
|
+
const unknown = [...required, ...optional].filter((field) => !def.fields[field]);
|
|
950
|
+
if (unknown.length) throw new Error(`defineChapter.formation: field "${unknown[0]}" is not declared on CRM type "${type}"`);
|
|
951
|
+
const missingRequired = Object.entries(def.fields).filter(([, field]) => field.required).map(([field]) => field).filter((field) => !required.includes(field));
|
|
952
|
+
if (missingRequired.length) {
|
|
953
|
+
throw new Error(`defineChapter.formation.required: must include required CRM field "${missingRequired[0]}"`);
|
|
954
|
+
}
|
|
955
|
+
const unsupportedRequired = required.find((field) => !["string", "email", "url", "enum"].includes(def.fields[field].type));
|
|
956
|
+
if (unsupportedRequired) {
|
|
957
|
+
throw new Error(`defineChapter.formation.required: field "${unsupportedRequired}" must be string-like`);
|
|
958
|
+
}
|
|
959
|
+
if (required.length === 0) throw new Error("defineChapter.formation.required: must not be empty");
|
|
960
|
+
const defaultMaxLen = input.defaultMaxLen ?? 4e3;
|
|
961
|
+
const bodyCap = input.bodyCap ?? 32768;
|
|
962
|
+
if (!Number.isSafeInteger(defaultMaxLen) || defaultMaxLen < 1 || defaultMaxLen > 32768) {
|
|
963
|
+
throw new Error("defineChapter.formation.defaultMaxLen: must be an integer from 1 to 32768");
|
|
964
|
+
}
|
|
965
|
+
if (!Number.isSafeInteger(bodyCap) || bodyCap < 1024 || bodyCap > 131072) {
|
|
966
|
+
throw new Error("defineChapter.formation.bodyCap: must be an integer from 1024 to 131072");
|
|
967
|
+
}
|
|
968
|
+
for (const [field, cap] of Object.entries(input.maxLen ?? {})) {
|
|
969
|
+
if (![...required, ...optional].includes(field) || !Number.isSafeInteger(cap) || cap < 1 || cap > 32768) {
|
|
970
|
+
throw new Error(`defineChapter.formation.maxLen.${field}: must name a public field and be an integer from 1 to 32768`);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
enabled: true,
|
|
975
|
+
type,
|
|
976
|
+
required,
|
|
977
|
+
optional,
|
|
978
|
+
maxLen: input.maxLen ?? {},
|
|
979
|
+
defaultMaxLen,
|
|
980
|
+
bodyCap
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
function formationFields(crm, formation) {
|
|
984
|
+
const def = crm.type(formation.type);
|
|
985
|
+
return [...formation.required, ...formation.optional].map((id2) => {
|
|
986
|
+
const field = def.fields[id2];
|
|
987
|
+
return {
|
|
988
|
+
id: id2,
|
|
989
|
+
label: field.label ?? id2,
|
|
990
|
+
type: field.type,
|
|
991
|
+
required: formation.required.includes(id2),
|
|
992
|
+
...field.options ? { options: field.options } : {}
|
|
993
|
+
};
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
|
|
902
997
|
// src/config.ts
|
|
903
998
|
var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
904
999
|
var FIELD = /^[a-z][a-zA-Z0-9_]*$/;
|
|
@@ -987,6 +1082,7 @@ function defineChapter(config) {
|
|
|
987
1082
|
const brand = { ...config.brand, wordmark: config.brand?.wordmark ?? name };
|
|
988
1083
|
const copy = resolveChapterCopy(config.copy);
|
|
989
1084
|
const network = resolveNetwork(config, crm);
|
|
1085
|
+
const formation = resolveLeaderFormation(config.formation, crm);
|
|
990
1086
|
const { schema, rules } = chapterDb(mode, auth);
|
|
991
1087
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
992
1088
|
const account = config.account ?? "none";
|
|
@@ -1042,6 +1138,7 @@ function defineChapter(config) {
|
|
|
1042
1138
|
brand,
|
|
1043
1139
|
copy,
|
|
1044
1140
|
network,
|
|
1141
|
+
formation,
|
|
1045
1142
|
crm,
|
|
1046
1143
|
auth,
|
|
1047
1144
|
pipeline,
|
|
@@ -1058,13 +1155,154 @@ function defineChapter(config) {
|
|
|
1058
1155
|
return chapter;
|
|
1059
1156
|
}
|
|
1060
1157
|
|
|
1061
|
-
// src/
|
|
1158
|
+
// src/leader.ts
|
|
1062
1159
|
var import_crm2 = require("@odla-ai/crm");
|
|
1160
|
+
function leaderCrmConfig(options = {}) {
|
|
1161
|
+
const base = options.base ?? defaultCrm("hub");
|
|
1162
|
+
const portfolio = {
|
|
1163
|
+
types: {
|
|
1164
|
+
chapter: {
|
|
1165
|
+
label: "Chapter",
|
|
1166
|
+
labelPlural: "Chapters",
|
|
1167
|
+
workspace: "portfolio",
|
|
1168
|
+
nameField: "name",
|
|
1169
|
+
fields: {
|
|
1170
|
+
name: { type: "string", label: "Name", required: true },
|
|
1171
|
+
slug: { type: "string", label: "Slug", slot: "s1" },
|
|
1172
|
+
region: { type: "string", label: "Region", slot: "s2" },
|
|
1173
|
+
url: { type: "url", label: "Website" },
|
|
1174
|
+
thesis: { type: "string", label: "Investment thesis" },
|
|
1175
|
+
launchedAt: { type: "date", label: "Launch date", slot: "d1" },
|
|
1176
|
+
notes: { type: "string", label: "Notes" }
|
|
1177
|
+
},
|
|
1178
|
+
pipeline: {
|
|
1179
|
+
stages: [
|
|
1180
|
+
{ id: "proposed", label: "Proposed" },
|
|
1181
|
+
{ id: "forming", label: "Forming" },
|
|
1182
|
+
{ id: "active", label: "Active" },
|
|
1183
|
+
{ id: "paused", label: "Paused" },
|
|
1184
|
+
{ id: "closed", label: "Closed", terminal: true }
|
|
1185
|
+
],
|
|
1186
|
+
transitions: {
|
|
1187
|
+
proposed: ["forming", "closed"],
|
|
1188
|
+
forming: ["active", "paused", "closed"],
|
|
1189
|
+
active: ["paused", "closed"],
|
|
1190
|
+
paused: ["forming", "active", "closed"]
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
facets: { rank: "manual" }
|
|
1194
|
+
},
|
|
1195
|
+
chapter_application: {
|
|
1196
|
+
label: "Chapter application",
|
|
1197
|
+
labelPlural: "Formation applications",
|
|
1198
|
+
workspace: "portfolio",
|
|
1199
|
+
nameField: "name",
|
|
1200
|
+
emailField: "email",
|
|
1201
|
+
fields: {
|
|
1202
|
+
name: { type: "string", label: "Applicant name", required: true },
|
|
1203
|
+
email: { type: "email", label: "Email", required: true },
|
|
1204
|
+
location: { type: "string", label: "Location", required: true, slot: "s1" },
|
|
1205
|
+
proposedChapter: { type: "string", label: "Proposed chapter", slot: "s2" },
|
|
1206
|
+
thesis: { type: "string", label: "Proposed thesis", required: true },
|
|
1207
|
+
experience: { type: "string", label: "Relevant experience" },
|
|
1208
|
+
collaborators: { type: "json", label: "Collaborators" },
|
|
1209
|
+
notes: { type: "string", label: "Internal notes" }
|
|
1210
|
+
},
|
|
1211
|
+
pipeline: {
|
|
1212
|
+
stages: [
|
|
1213
|
+
{ id: "submitted", label: "Submitted" },
|
|
1214
|
+
{ id: "reviewing", label: "Reviewing" },
|
|
1215
|
+
{ id: "interviewing", label: "Interviewing" },
|
|
1216
|
+
{ id: "approved", label: "Approved" },
|
|
1217
|
+
{ id: "forming", label: "Forming" },
|
|
1218
|
+
{ id: "declined", label: "Declined", terminal: true }
|
|
1219
|
+
],
|
|
1220
|
+
transitions: {
|
|
1221
|
+
submitted: ["reviewing", "declined"],
|
|
1222
|
+
reviewing: ["interviewing", "approved", "declined"],
|
|
1223
|
+
interviewing: ["approved", "declined"],
|
|
1224
|
+
approved: ["forming", "declined"]
|
|
1225
|
+
}
|
|
1226
|
+
},
|
|
1227
|
+
facets: { email: true, rank: "manual" }
|
|
1228
|
+
},
|
|
1229
|
+
deal: {
|
|
1230
|
+
label: "Deal",
|
|
1231
|
+
labelPlural: "Deal flow",
|
|
1232
|
+
workspace: "portfolio",
|
|
1233
|
+
nameField: "name",
|
|
1234
|
+
fields: {
|
|
1235
|
+
name: { type: "string", label: "Company or deal", required: true },
|
|
1236
|
+
chapter: { type: "string", label: "Source chapter", slot: "s1" },
|
|
1237
|
+
sector: { type: "string", label: "Sector", slot: "s2" },
|
|
1238
|
+
source: { type: "string", label: "Source", slot: "s3" },
|
|
1239
|
+
website: { type: "url", label: "Website" },
|
|
1240
|
+
amount: { type: "number", label: "Target amount", slot: "n1" },
|
|
1241
|
+
notes: { type: "string", label: "Notes" }
|
|
1242
|
+
},
|
|
1243
|
+
pipeline: {
|
|
1244
|
+
stages: [
|
|
1245
|
+
{ id: "sourced", label: "Sourced" },
|
|
1246
|
+
{ id: "screening", label: "Screening" },
|
|
1247
|
+
{ id: "diligence", label: "Diligence" },
|
|
1248
|
+
{ id: "committee", label: "Committee" },
|
|
1249
|
+
{ id: "offered", label: "Offered" },
|
|
1250
|
+
{ id: "invested", label: "Invested", terminal: true },
|
|
1251
|
+
{ id: "passed", label: "Passed", terminal: true }
|
|
1252
|
+
],
|
|
1253
|
+
transitions: {
|
|
1254
|
+
sourced: ["screening", "passed"],
|
|
1255
|
+
screening: ["diligence", "committee", "passed"],
|
|
1256
|
+
diligence: ["committee", "offered", "passed"],
|
|
1257
|
+
committee: ["offered", "passed"],
|
|
1258
|
+
offered: ["invested", "passed"],
|
|
1259
|
+
passed: ["sourced"]
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
1262
|
+
facets: { rank: "manual" }
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
relations: {
|
|
1266
|
+
formation_result: {
|
|
1267
|
+
from: "chapter_application",
|
|
1268
|
+
to: "chapter",
|
|
1269
|
+
label: "formed as",
|
|
1270
|
+
reverseLabel: "formation application"
|
|
1271
|
+
},
|
|
1272
|
+
chapter_deal: {
|
|
1273
|
+
from: "deal",
|
|
1274
|
+
to: "chapter",
|
|
1275
|
+
label: "sourced by",
|
|
1276
|
+
reverseLabel: "deal flow"
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
if (base.types.company) {
|
|
1281
|
+
portfolio.relations.deal_company = {
|
|
1282
|
+
from: "deal",
|
|
1283
|
+
to: "company",
|
|
1284
|
+
label: "concerns",
|
|
1285
|
+
reverseLabel: "deals"
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
if (base.types.person) {
|
|
1289
|
+
portfolio.relations.application_contact = {
|
|
1290
|
+
from: "person",
|
|
1291
|
+
to: "chapter_application",
|
|
1292
|
+
label: "submitted",
|
|
1293
|
+
reverseLabel: "applicant"
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
return (0, import_crm2.composeCrmConfigs)(base, portfolio);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// src/descriptor.ts
|
|
1300
|
+
var import_crm3 = require("@odla-ai/crm");
|
|
1063
1301
|
function createChapterIntegration(chapter, options = {}) {
|
|
1064
1302
|
const basePath = options.basePath ?? "/api/crm";
|
|
1065
1303
|
const now = options.now ?? Date.now();
|
|
1066
1304
|
const emails = chapter.config.emails;
|
|
1067
|
-
const crmDesc = (0,
|
|
1305
|
+
const crmDesc = (0, import_crm3.createCrmIntegration)(chapter.crm, {
|
|
1068
1306
|
basePath,
|
|
1069
1307
|
now,
|
|
1070
1308
|
...emails?.notificationEmail ? { notificationEmail: emails.notificationEmail } : {},
|
|
@@ -1096,7 +1334,15 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
1096
1334
|
},
|
|
1097
1335
|
rules: { ...crmDesc.rules, ...chapter.rules },
|
|
1098
1336
|
seeds,
|
|
1099
|
-
probes: [
|
|
1337
|
+
probes: [
|
|
1338
|
+
...crmDesc.probes ?? [],
|
|
1339
|
+
// The snapshot route is deliberately private. A credential-free smoke
|
|
1340
|
+
// request must prove that the route is mounted and closed.
|
|
1341
|
+
{ path: "/api/network/snapshot", expectedStatus: 401 },
|
|
1342
|
+
// Formation metadata is public only when the host explicitly enables
|
|
1343
|
+
// the bounded intake contract.
|
|
1344
|
+
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : []
|
|
1345
|
+
],
|
|
1100
1346
|
runbooks
|
|
1101
1347
|
};
|
|
1102
1348
|
}
|
|
@@ -1300,11 +1546,15 @@ function canceledPatch() {
|
|
|
1300
1546
|
}
|
|
1301
1547
|
|
|
1302
1548
|
// src/network.ts
|
|
1303
|
-
var
|
|
1549
|
+
var import_crm4 = require("@odla-ai/crm");
|
|
1550
|
+
|
|
1551
|
+
// src/network-contract.ts
|
|
1304
1552
|
var DEFAULT_SHARE_FIELDS = {
|
|
1305
1553
|
person: ["name", "email", "firstName", "lastName", "phone", "linkedin"],
|
|
1306
1554
|
company: ["name", "domain", "industry", "location", "linkedin", "notes"]
|
|
1307
1555
|
};
|
|
1556
|
+
|
|
1557
|
+
// src/network.ts
|
|
1308
1558
|
function sharedPersonInput(person) {
|
|
1309
1559
|
const email = person.email.toLowerCase();
|
|
1310
1560
|
const fullName = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
|
|
@@ -1326,25 +1576,55 @@ function shortHash(value) {
|
|
|
1326
1576
|
}
|
|
1327
1577
|
return `${(a >>> 0).toString(36)}${(b >>> 0).toString(36)}`;
|
|
1328
1578
|
}
|
|
1329
|
-
function networkSourceTag(type, hubRecordId) {
|
|
1579
|
+
function networkSourceTag(type, hubRecordId, sourceId) {
|
|
1330
1580
|
const typeKey = type.toLowerCase();
|
|
1331
1581
|
const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
|
|
1332
|
-
const
|
|
1582
|
+
const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
|
|
1583
|
+
const raw = `${prefix}:${hubRecordId}`;
|
|
1333
1584
|
if (readable && raw.length <= 64) return raw;
|
|
1334
|
-
return `network:${typeKey.slice(0,
|
|
1335
|
-
}
|
|
1336
|
-
function normalizeSharedRecord(record) {
|
|
1337
|
-
if ("input" in record
|
|
1585
|
+
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
1586
|
+
}
|
|
1587
|
+
function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
|
|
1588
|
+
if ("input" in record && record.version === 2) {
|
|
1589
|
+
return {
|
|
1590
|
+
version: 2,
|
|
1591
|
+
sourceId: record.source.siteId,
|
|
1592
|
+
sourceRecordId: record.source.recordId,
|
|
1593
|
+
type: record.type,
|
|
1594
|
+
input: record.input
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
if ("input" in record) {
|
|
1598
|
+
return {
|
|
1599
|
+
version: 1,
|
|
1600
|
+
sourceId: fallbackSourceId,
|
|
1601
|
+
sourceRecordId: record.hubRecordId,
|
|
1602
|
+
type: record.type,
|
|
1603
|
+
input: record.input
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1338
1606
|
if ("type" in record && record.type === "company") {
|
|
1339
1607
|
const input = { name: record.name };
|
|
1340
1608
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
1341
1609
|
if (record[key]) input[key] = record[key];
|
|
1342
1610
|
}
|
|
1343
|
-
return {
|
|
1611
|
+
return {
|
|
1612
|
+
version: 1,
|
|
1613
|
+
sourceId: fallbackSourceId,
|
|
1614
|
+
sourceRecordId: record.hubRecordId,
|
|
1615
|
+
type: "company",
|
|
1616
|
+
input
|
|
1617
|
+
};
|
|
1344
1618
|
}
|
|
1345
|
-
return {
|
|
1619
|
+
return {
|
|
1620
|
+
version: 1,
|
|
1621
|
+
sourceId: fallbackSourceId,
|
|
1622
|
+
sourceRecordId: record.hubRecordId,
|
|
1623
|
+
type: "person",
|
|
1624
|
+
input: sharedPersonInput(record)
|
|
1625
|
+
};
|
|
1346
1626
|
}
|
|
1347
|
-
function sharedRecordFromCrm(crm, record, target) {
|
|
1627
|
+
function sharedRecordFromCrm(crm, record, target, sourceId) {
|
|
1348
1628
|
if (target.fields && !target.fields[record.type]) {
|
|
1349
1629
|
throw new Error(`${target.name} does not accept "${record.type}" records`);
|
|
1350
1630
|
}
|
|
@@ -1360,7 +1640,7 @@ function sharedRecordFromCrm(crm, record, target) {
|
|
|
1360
1640
|
if (value !== void 0) input[field] = value;
|
|
1361
1641
|
}
|
|
1362
1642
|
if (input[nameField] === void 0) input[nameField] = record.name;
|
|
1363
|
-
return { version: 1, type: record.type, hubRecordId: record.id, input };
|
|
1643
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record.id }, type: record.type, input } : { version: 1, type: record.type, hubRecordId: record.id, input };
|
|
1364
1644
|
}
|
|
1365
1645
|
async function upsertPerson(deps, opts) {
|
|
1366
1646
|
const email = opts.email.toLowerCase();
|
|
@@ -1368,13 +1648,19 @@ async function upsertPerson(deps, opts) {
|
|
|
1368
1648
|
const { crm_record } = await deps.db.query({ crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } } });
|
|
1369
1649
|
const existing = crm_record?.[0];
|
|
1370
1650
|
if (existing && typeof existing.id === "string") {
|
|
1371
|
-
await (0,
|
|
1651
|
+
await (0, import_crm4.updateRecord)(crmDeps, { id: existing.id, input: opts.input });
|
|
1372
1652
|
return { recordId: existing.id };
|
|
1373
1653
|
}
|
|
1374
|
-
const created = await (0,
|
|
1654
|
+
const created = await (0, import_crm4.createRecord)(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
1375
1655
|
return { recordId: created.id };
|
|
1376
1656
|
}
|
|
1377
1657
|
async function findSharedRecord(deps, record, tag) {
|
|
1658
|
+
const structured = await (0, import_crm4.getRecordByOrigin)(
|
|
1659
|
+
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
1660
|
+
record.sourceId,
|
|
1661
|
+
record.sourceRecordId
|
|
1662
|
+
);
|
|
1663
|
+
if (structured) return structured;
|
|
1378
1664
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
1379
1665
|
const mappedId = mapped.crm_tag?.[0]?.recordId;
|
|
1380
1666
|
if (typeof mappedId === "string") {
|
|
@@ -1408,21 +1694,24 @@ async function findSharedRecord(deps, record, tag) {
|
|
|
1408
1694
|
}
|
|
1409
1695
|
return void 0;
|
|
1410
1696
|
}
|
|
1411
|
-
async function projectSharedRecord(deps, shared) {
|
|
1412
|
-
const record = normalizeSharedRecord(shared);
|
|
1413
|
-
if (!record.type.trim() || !record.
|
|
1414
|
-
throw new Error("type and
|
|
1697
|
+
async function projectSharedRecord(deps, shared, options = {}) {
|
|
1698
|
+
const record = normalizeSharedRecord(shared, options.sourceId);
|
|
1699
|
+
if (!record.type.trim() || !record.sourceId.trim() || !record.sourceRecordId.trim()) {
|
|
1700
|
+
throw new Error("type, source site id, and source record id must be non-empty");
|
|
1701
|
+
}
|
|
1702
|
+
if (options.sourceId && options.sourceId !== record.sourceId) {
|
|
1703
|
+
throw new Error("signed sender does not match payload source");
|
|
1415
1704
|
}
|
|
1416
|
-
const tag = networkSourceTag(record.type, record.
|
|
1705
|
+
const tag = networkSourceTag(record.type, record.sourceRecordId, record.version === 2 ? record.sourceId : void 0);
|
|
1417
1706
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1418
1707
|
const existing = await findSharedRecord(deps, record, tag);
|
|
1419
1708
|
let recordId;
|
|
1420
1709
|
if (existing && typeof existing.id === "string") {
|
|
1421
|
-
await (0,
|
|
1710
|
+
await (0, import_crm4.updateRecord)(crmDeps, { id: existing.id, input: record.input });
|
|
1422
1711
|
recordId = existing.id;
|
|
1423
1712
|
} else {
|
|
1424
|
-
recordId = `network_${shortHash(`${record.type}\0${record.
|
|
1425
|
-
await (0,
|
|
1713
|
+
recordId = `network_${shortHash(`${record.sourceId}\0${record.type}\0${record.sourceRecordId}`)}`;
|
|
1714
|
+
await (0, import_crm4.createRecord)({ ...crmDeps, newId: () => recordId }, {
|
|
1426
1715
|
type: record.type,
|
|
1427
1716
|
input: record.input,
|
|
1428
1717
|
mutationId: `share-create:${tag}`
|
|
@@ -1432,6 +1721,13 @@ async function projectSharedRecord(deps, shared) {
|
|
|
1432
1721
|
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
|
|
1433
1722
|
{ mutationId: `share-map:${tag}:${recordId}` }
|
|
1434
1723
|
);
|
|
1724
|
+
await (0, import_crm4.upsertRecordOrigin)(crmDeps, {
|
|
1725
|
+
recordId,
|
|
1726
|
+
sourceId: record.sourceId,
|
|
1727
|
+
sourceRecordId: record.sourceRecordId,
|
|
1728
|
+
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
1729
|
+
payloadVersion: record.version
|
|
1730
|
+
});
|
|
1435
1731
|
return { recordId };
|
|
1436
1732
|
}
|
|
1437
1733
|
async function projectApplicant(deps, applicant) {
|
|
@@ -1454,7 +1750,7 @@ async function projectApplicant(deps, applicant) {
|
|
|
1454
1750
|
}
|
|
1455
1751
|
|
|
1456
1752
|
// src/crm-sync.ts
|
|
1457
|
-
var
|
|
1753
|
+
var import_crm5 = require("@odla-ai/crm");
|
|
1458
1754
|
var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
|
|
1459
1755
|
function personInputFromApp(chapter, app) {
|
|
1460
1756
|
const input = sharedPersonInput({
|
|
@@ -1494,16 +1790,16 @@ async function syncApplicationToCrm(deps, opts) {
|
|
|
1494
1790
|
let recordId;
|
|
1495
1791
|
if (existing && typeof existing.id === "string") {
|
|
1496
1792
|
recordId = existing.id;
|
|
1497
|
-
await (0,
|
|
1793
|
+
await (0, import_crm5.updateRecord)(recordDeps, { id: recordId, input });
|
|
1498
1794
|
} else {
|
|
1499
|
-
const created = await (0,
|
|
1795
|
+
const created = await (0, import_crm5.createRecord)(recordDeps, { type: "person", input, ...stage ? { stage } : {} });
|
|
1500
1796
|
recordId = created.id;
|
|
1501
1797
|
}
|
|
1502
1798
|
if (existing && stage && existing.stage !== stage) {
|
|
1503
|
-
await (0,
|
|
1799
|
+
await (0, import_crm5.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
|
|
1504
1800
|
}
|
|
1505
1801
|
await deps.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
|
|
1506
|
-
await (0,
|
|
1802
|
+
await (0, import_crm5.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
|
|
1507
1803
|
return recordId;
|
|
1508
1804
|
}
|
|
1509
1805
|
async function backfillCrm(deps) {
|