@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.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,9 +919,85 @@ 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_]*$/;
|
|
1000
|
+
var BINDING = /^[A-Z][A-Z0-9_]{0,127}$/;
|
|
905
1001
|
function resolveNetwork(config, crm) {
|
|
906
1002
|
const seen = /* @__PURE__ */ new Set();
|
|
907
1003
|
const targets = [];
|
|
@@ -947,10 +1043,17 @@ function resolveNetwork(config, crm) {
|
|
|
947
1043
|
if (!/^[a-zA-Z][a-zA-Z0-9_-]{1,127}$/.test(secretName)) {
|
|
948
1044
|
throw new Error(`defineChapter.network.targets.${target.id}.secretName: must be a vault-key identifier`);
|
|
949
1045
|
}
|
|
1046
|
+
const binding = target.binding?.trim();
|
|
1047
|
+
if (binding && !BINDING.test(binding)) {
|
|
1048
|
+
throw new Error(
|
|
1049
|
+
`defineChapter.network.targets.${target.id}.binding: must be an uppercase Worker binding identifier`
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
950
1052
|
targets.push({
|
|
951
1053
|
id: target.id,
|
|
952
1054
|
name: target.name?.trim() || target.id,
|
|
953
1055
|
url: url.origin,
|
|
1056
|
+
...binding ? { binding } : {},
|
|
954
1057
|
secretName,
|
|
955
1058
|
...fields ? { fields } : {}
|
|
956
1059
|
});
|
|
@@ -987,6 +1090,7 @@ function defineChapter(config) {
|
|
|
987
1090
|
const brand = { ...config.brand, wordmark: config.brand?.wordmark ?? name };
|
|
988
1091
|
const copy = resolveChapterCopy(config.copy);
|
|
989
1092
|
const network = resolveNetwork(config, crm);
|
|
1093
|
+
const formation = resolveLeaderFormation(config.formation, crm);
|
|
990
1094
|
const { schema, rules } = chapterDb(mode, auth);
|
|
991
1095
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
992
1096
|
const account = config.account ?? "none";
|
|
@@ -1042,6 +1146,7 @@ function defineChapter(config) {
|
|
|
1042
1146
|
brand,
|
|
1043
1147
|
copy,
|
|
1044
1148
|
network,
|
|
1149
|
+
formation,
|
|
1045
1150
|
crm,
|
|
1046
1151
|
auth,
|
|
1047
1152
|
pipeline,
|
|
@@ -1058,13 +1163,154 @@ function defineChapter(config) {
|
|
|
1058
1163
|
return chapter;
|
|
1059
1164
|
}
|
|
1060
1165
|
|
|
1061
|
-
// src/
|
|
1166
|
+
// src/leader.ts
|
|
1062
1167
|
var import_crm2 = require("@odla-ai/crm");
|
|
1168
|
+
function leaderCrmConfig(options = {}) {
|
|
1169
|
+
const base = options.base ?? defaultCrm("hub");
|
|
1170
|
+
const portfolio = {
|
|
1171
|
+
types: {
|
|
1172
|
+
chapter: {
|
|
1173
|
+
label: "Chapter",
|
|
1174
|
+
labelPlural: "Chapters",
|
|
1175
|
+
workspace: "portfolio",
|
|
1176
|
+
nameField: "name",
|
|
1177
|
+
fields: {
|
|
1178
|
+
name: { type: "string", label: "Name", required: true },
|
|
1179
|
+
slug: { type: "string", label: "Slug", slot: "s1" },
|
|
1180
|
+
region: { type: "string", label: "Region", slot: "s2" },
|
|
1181
|
+
url: { type: "url", label: "Website" },
|
|
1182
|
+
thesis: { type: "string", label: "Investment thesis" },
|
|
1183
|
+
launchedAt: { type: "date", label: "Launch date", slot: "d1" },
|
|
1184
|
+
notes: { type: "string", label: "Notes" }
|
|
1185
|
+
},
|
|
1186
|
+
pipeline: {
|
|
1187
|
+
stages: [
|
|
1188
|
+
{ id: "proposed", label: "Proposed" },
|
|
1189
|
+
{ id: "forming", label: "Forming" },
|
|
1190
|
+
{ id: "active", label: "Active" },
|
|
1191
|
+
{ id: "paused", label: "Paused" },
|
|
1192
|
+
{ id: "closed", label: "Closed", terminal: true }
|
|
1193
|
+
],
|
|
1194
|
+
transitions: {
|
|
1195
|
+
proposed: ["forming", "closed"],
|
|
1196
|
+
forming: ["active", "paused", "closed"],
|
|
1197
|
+
active: ["paused", "closed"],
|
|
1198
|
+
paused: ["forming", "active", "closed"]
|
|
1199
|
+
}
|
|
1200
|
+
},
|
|
1201
|
+
facets: { rank: "manual" }
|
|
1202
|
+
},
|
|
1203
|
+
chapter_application: {
|
|
1204
|
+
label: "Chapter application",
|
|
1205
|
+
labelPlural: "Formation applications",
|
|
1206
|
+
workspace: "portfolio",
|
|
1207
|
+
nameField: "name",
|
|
1208
|
+
emailField: "email",
|
|
1209
|
+
fields: {
|
|
1210
|
+
name: { type: "string", label: "Applicant name", required: true },
|
|
1211
|
+
email: { type: "email", label: "Email", required: true },
|
|
1212
|
+
location: { type: "string", label: "Location", required: true, slot: "s1" },
|
|
1213
|
+
proposedChapter: { type: "string", label: "Proposed chapter", slot: "s2" },
|
|
1214
|
+
thesis: { type: "string", label: "Proposed thesis", required: true },
|
|
1215
|
+
experience: { type: "string", label: "Relevant experience" },
|
|
1216
|
+
collaborators: { type: "json", label: "Collaborators" },
|
|
1217
|
+
notes: { type: "string", label: "Internal notes" }
|
|
1218
|
+
},
|
|
1219
|
+
pipeline: {
|
|
1220
|
+
stages: [
|
|
1221
|
+
{ id: "submitted", label: "Submitted" },
|
|
1222
|
+
{ id: "reviewing", label: "Reviewing" },
|
|
1223
|
+
{ id: "interviewing", label: "Interviewing" },
|
|
1224
|
+
{ id: "approved", label: "Approved" },
|
|
1225
|
+
{ id: "forming", label: "Forming" },
|
|
1226
|
+
{ id: "declined", label: "Declined", terminal: true }
|
|
1227
|
+
],
|
|
1228
|
+
transitions: {
|
|
1229
|
+
submitted: ["reviewing", "declined"],
|
|
1230
|
+
reviewing: ["interviewing", "approved", "declined"],
|
|
1231
|
+
interviewing: ["approved", "declined"],
|
|
1232
|
+
approved: ["forming", "declined"]
|
|
1233
|
+
}
|
|
1234
|
+
},
|
|
1235
|
+
facets: { email: true, rank: "manual" }
|
|
1236
|
+
},
|
|
1237
|
+
deal: {
|
|
1238
|
+
label: "Deal",
|
|
1239
|
+
labelPlural: "Deal flow",
|
|
1240
|
+
workspace: "portfolio",
|
|
1241
|
+
nameField: "name",
|
|
1242
|
+
fields: {
|
|
1243
|
+
name: { type: "string", label: "Company or deal", required: true },
|
|
1244
|
+
chapter: { type: "string", label: "Source chapter", slot: "s1" },
|
|
1245
|
+
sector: { type: "string", label: "Sector", slot: "s2" },
|
|
1246
|
+
source: { type: "string", label: "Source", slot: "s3" },
|
|
1247
|
+
website: { type: "url", label: "Website" },
|
|
1248
|
+
amount: { type: "number", label: "Target amount", slot: "n1" },
|
|
1249
|
+
notes: { type: "string", label: "Notes" }
|
|
1250
|
+
},
|
|
1251
|
+
pipeline: {
|
|
1252
|
+
stages: [
|
|
1253
|
+
{ id: "sourced", label: "Sourced" },
|
|
1254
|
+
{ id: "screening", label: "Screening" },
|
|
1255
|
+
{ id: "diligence", label: "Diligence" },
|
|
1256
|
+
{ id: "committee", label: "Committee" },
|
|
1257
|
+
{ id: "offered", label: "Offered" },
|
|
1258
|
+
{ id: "invested", label: "Invested", terminal: true },
|
|
1259
|
+
{ id: "passed", label: "Passed", terminal: true }
|
|
1260
|
+
],
|
|
1261
|
+
transitions: {
|
|
1262
|
+
sourced: ["screening", "passed"],
|
|
1263
|
+
screening: ["diligence", "committee", "passed"],
|
|
1264
|
+
diligence: ["committee", "offered", "passed"],
|
|
1265
|
+
committee: ["offered", "passed"],
|
|
1266
|
+
offered: ["invested", "passed"],
|
|
1267
|
+
passed: ["sourced"]
|
|
1268
|
+
}
|
|
1269
|
+
},
|
|
1270
|
+
facets: { rank: "manual" }
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
relations: {
|
|
1274
|
+
formation_result: {
|
|
1275
|
+
from: "chapter_application",
|
|
1276
|
+
to: "chapter",
|
|
1277
|
+
label: "formed as",
|
|
1278
|
+
reverseLabel: "formation application"
|
|
1279
|
+
},
|
|
1280
|
+
chapter_deal: {
|
|
1281
|
+
from: "deal",
|
|
1282
|
+
to: "chapter",
|
|
1283
|
+
label: "sourced by",
|
|
1284
|
+
reverseLabel: "deal flow"
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
if (base.types.company) {
|
|
1289
|
+
portfolio.relations.deal_company = {
|
|
1290
|
+
from: "deal",
|
|
1291
|
+
to: "company",
|
|
1292
|
+
label: "concerns",
|
|
1293
|
+
reverseLabel: "deals"
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
if (base.types.person) {
|
|
1297
|
+
portfolio.relations.application_contact = {
|
|
1298
|
+
from: "person",
|
|
1299
|
+
to: "chapter_application",
|
|
1300
|
+
label: "submitted",
|
|
1301
|
+
reverseLabel: "applicant"
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
return (0, import_crm2.composeCrmConfigs)(base, portfolio);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// src/descriptor.ts
|
|
1308
|
+
var import_crm3 = require("@odla-ai/crm");
|
|
1063
1309
|
function createChapterIntegration(chapter, options = {}) {
|
|
1064
1310
|
const basePath = options.basePath ?? "/api/crm";
|
|
1065
1311
|
const now = options.now ?? Date.now();
|
|
1066
1312
|
const emails = chapter.config.emails;
|
|
1067
|
-
const crmDesc = (0,
|
|
1313
|
+
const crmDesc = (0, import_crm3.createCrmIntegration)(chapter.crm, {
|
|
1068
1314
|
basePath,
|
|
1069
1315
|
now,
|
|
1070
1316
|
...emails?.notificationEmail ? { notificationEmail: emails.notificationEmail } : {},
|
|
@@ -1096,7 +1342,15 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
1096
1342
|
},
|
|
1097
1343
|
rules: { ...crmDesc.rules, ...chapter.rules },
|
|
1098
1344
|
seeds,
|
|
1099
|
-
probes: [
|
|
1345
|
+
probes: [
|
|
1346
|
+
...crmDesc.probes ?? [],
|
|
1347
|
+
// The snapshot route is deliberately private. A credential-free smoke
|
|
1348
|
+
// request must prove that the route is mounted and closed.
|
|
1349
|
+
{ path: "/api/network/snapshot", expectedStatus: 401 },
|
|
1350
|
+
// Formation metadata is public only when the host explicitly enables
|
|
1351
|
+
// the bounded intake contract.
|
|
1352
|
+
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : []
|
|
1353
|
+
],
|
|
1100
1354
|
runbooks
|
|
1101
1355
|
};
|
|
1102
1356
|
}
|
|
@@ -1300,11 +1554,15 @@ function canceledPatch() {
|
|
|
1300
1554
|
}
|
|
1301
1555
|
|
|
1302
1556
|
// src/network.ts
|
|
1303
|
-
var
|
|
1557
|
+
var import_crm4 = require("@odla-ai/crm");
|
|
1558
|
+
|
|
1559
|
+
// src/network-contract.ts
|
|
1304
1560
|
var DEFAULT_SHARE_FIELDS = {
|
|
1305
1561
|
person: ["name", "email", "firstName", "lastName", "phone", "linkedin"],
|
|
1306
1562
|
company: ["name", "domain", "industry", "location", "linkedin", "notes"]
|
|
1307
1563
|
};
|
|
1564
|
+
|
|
1565
|
+
// src/network.ts
|
|
1308
1566
|
function sharedPersonInput(person) {
|
|
1309
1567
|
const email = person.email.toLowerCase();
|
|
1310
1568
|
const fullName = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
|
|
@@ -1326,25 +1584,55 @@ function shortHash(value) {
|
|
|
1326
1584
|
}
|
|
1327
1585
|
return `${(a >>> 0).toString(36)}${(b >>> 0).toString(36)}`;
|
|
1328
1586
|
}
|
|
1329
|
-
function networkSourceTag(type, hubRecordId) {
|
|
1587
|
+
function networkSourceTag(type, hubRecordId, sourceId) {
|
|
1330
1588
|
const typeKey = type.toLowerCase();
|
|
1331
1589
|
const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
|
|
1332
|
-
const
|
|
1590
|
+
const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
|
|
1591
|
+
const raw = `${prefix}:${hubRecordId}`;
|
|
1333
1592
|
if (readable && raw.length <= 64) return raw;
|
|
1334
|
-
return `network:${typeKey.slice(0,
|
|
1335
|
-
}
|
|
1336
|
-
function normalizeSharedRecord(record) {
|
|
1337
|
-
if ("input" in record
|
|
1593
|
+
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
1594
|
+
}
|
|
1595
|
+
function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
|
|
1596
|
+
if ("input" in record && record.version === 2) {
|
|
1597
|
+
return {
|
|
1598
|
+
version: 2,
|
|
1599
|
+
sourceId: record.source.siteId,
|
|
1600
|
+
sourceRecordId: record.source.recordId,
|
|
1601
|
+
type: record.type,
|
|
1602
|
+
input: record.input
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
if ("input" in record) {
|
|
1606
|
+
return {
|
|
1607
|
+
version: 1,
|
|
1608
|
+
sourceId: fallbackSourceId,
|
|
1609
|
+
sourceRecordId: record.hubRecordId,
|
|
1610
|
+
type: record.type,
|
|
1611
|
+
input: record.input
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1338
1614
|
if ("type" in record && record.type === "company") {
|
|
1339
1615
|
const input = { name: record.name };
|
|
1340
1616
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
1341
1617
|
if (record[key]) input[key] = record[key];
|
|
1342
1618
|
}
|
|
1343
|
-
return {
|
|
1619
|
+
return {
|
|
1620
|
+
version: 1,
|
|
1621
|
+
sourceId: fallbackSourceId,
|
|
1622
|
+
sourceRecordId: record.hubRecordId,
|
|
1623
|
+
type: "company",
|
|
1624
|
+
input
|
|
1625
|
+
};
|
|
1344
1626
|
}
|
|
1345
|
-
return {
|
|
1627
|
+
return {
|
|
1628
|
+
version: 1,
|
|
1629
|
+
sourceId: fallbackSourceId,
|
|
1630
|
+
sourceRecordId: record.hubRecordId,
|
|
1631
|
+
type: "person",
|
|
1632
|
+
input: sharedPersonInput(record)
|
|
1633
|
+
};
|
|
1346
1634
|
}
|
|
1347
|
-
function sharedRecordFromCrm(crm, record, target) {
|
|
1635
|
+
function sharedRecordFromCrm(crm, record, target, sourceId) {
|
|
1348
1636
|
if (target.fields && !target.fields[record.type]) {
|
|
1349
1637
|
throw new Error(`${target.name} does not accept "${record.type}" records`);
|
|
1350
1638
|
}
|
|
@@ -1360,7 +1648,7 @@ function sharedRecordFromCrm(crm, record, target) {
|
|
|
1360
1648
|
if (value !== void 0) input[field] = value;
|
|
1361
1649
|
}
|
|
1362
1650
|
if (input[nameField] === void 0) input[nameField] = record.name;
|
|
1363
|
-
return { version: 1, type: record.type, hubRecordId: record.id, input };
|
|
1651
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record.id }, type: record.type, input } : { version: 1, type: record.type, hubRecordId: record.id, input };
|
|
1364
1652
|
}
|
|
1365
1653
|
async function upsertPerson(deps, opts) {
|
|
1366
1654
|
const email = opts.email.toLowerCase();
|
|
@@ -1368,13 +1656,19 @@ async function upsertPerson(deps, opts) {
|
|
|
1368
1656
|
const { crm_record } = await deps.db.query({ crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } } });
|
|
1369
1657
|
const existing = crm_record?.[0];
|
|
1370
1658
|
if (existing && typeof existing.id === "string") {
|
|
1371
|
-
await (0,
|
|
1659
|
+
await (0, import_crm4.updateRecord)(crmDeps, { id: existing.id, input: opts.input });
|
|
1372
1660
|
return { recordId: existing.id };
|
|
1373
1661
|
}
|
|
1374
|
-
const created = await (0,
|
|
1662
|
+
const created = await (0, import_crm4.createRecord)(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
1375
1663
|
return { recordId: created.id };
|
|
1376
1664
|
}
|
|
1377
1665
|
async function findSharedRecord(deps, record, tag) {
|
|
1666
|
+
const structured = await (0, import_crm4.getRecordByOrigin)(
|
|
1667
|
+
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
1668
|
+
record.sourceId,
|
|
1669
|
+
record.sourceRecordId
|
|
1670
|
+
);
|
|
1671
|
+
if (structured) return structured;
|
|
1378
1672
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
1379
1673
|
const mappedId = mapped.crm_tag?.[0]?.recordId;
|
|
1380
1674
|
if (typeof mappedId === "string") {
|
|
@@ -1408,21 +1702,24 @@ async function findSharedRecord(deps, record, tag) {
|
|
|
1408
1702
|
}
|
|
1409
1703
|
return void 0;
|
|
1410
1704
|
}
|
|
1411
|
-
async function projectSharedRecord(deps, shared) {
|
|
1412
|
-
const record = normalizeSharedRecord(shared);
|
|
1413
|
-
if (!record.type.trim() || !record.
|
|
1414
|
-
throw new Error("type and
|
|
1705
|
+
async function projectSharedRecord(deps, shared, options = {}) {
|
|
1706
|
+
const record = normalizeSharedRecord(shared, options.sourceId);
|
|
1707
|
+
if (!record.type.trim() || !record.sourceId.trim() || !record.sourceRecordId.trim()) {
|
|
1708
|
+
throw new Error("type, source site id, and source record id must be non-empty");
|
|
1709
|
+
}
|
|
1710
|
+
if (options.sourceId && options.sourceId !== record.sourceId) {
|
|
1711
|
+
throw new Error("signed sender does not match payload source");
|
|
1415
1712
|
}
|
|
1416
|
-
const tag = networkSourceTag(record.type, record.
|
|
1713
|
+
const tag = networkSourceTag(record.type, record.sourceRecordId, record.version === 2 ? record.sourceId : void 0);
|
|
1417
1714
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1418
1715
|
const existing = await findSharedRecord(deps, record, tag);
|
|
1419
1716
|
let recordId;
|
|
1420
1717
|
if (existing && typeof existing.id === "string") {
|
|
1421
|
-
await (0,
|
|
1718
|
+
await (0, import_crm4.updateRecord)(crmDeps, { id: existing.id, input: record.input });
|
|
1422
1719
|
recordId = existing.id;
|
|
1423
1720
|
} else {
|
|
1424
|
-
recordId = `network_${shortHash(`${record.type}\0${record.
|
|
1425
|
-
await (0,
|
|
1721
|
+
recordId = `network_${shortHash(`${record.sourceId}\0${record.type}\0${record.sourceRecordId}`)}`;
|
|
1722
|
+
await (0, import_crm4.createRecord)({ ...crmDeps, newId: () => recordId }, {
|
|
1426
1723
|
type: record.type,
|
|
1427
1724
|
input: record.input,
|
|
1428
1725
|
mutationId: `share-create:${tag}`
|
|
@@ -1432,6 +1729,13 @@ async function projectSharedRecord(deps, shared) {
|
|
|
1432
1729
|
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
|
|
1433
1730
|
{ mutationId: `share-map:${tag}:${recordId}` }
|
|
1434
1731
|
);
|
|
1732
|
+
await (0, import_crm4.upsertRecordOrigin)(crmDeps, {
|
|
1733
|
+
recordId,
|
|
1734
|
+
sourceId: record.sourceId,
|
|
1735
|
+
sourceRecordId: record.sourceRecordId,
|
|
1736
|
+
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
1737
|
+
payloadVersion: record.version
|
|
1738
|
+
});
|
|
1435
1739
|
return { recordId };
|
|
1436
1740
|
}
|
|
1437
1741
|
async function projectApplicant(deps, applicant) {
|
|
@@ -1454,7 +1758,7 @@ async function projectApplicant(deps, applicant) {
|
|
|
1454
1758
|
}
|
|
1455
1759
|
|
|
1456
1760
|
// src/crm-sync.ts
|
|
1457
|
-
var
|
|
1761
|
+
var import_crm5 = require("@odla-ai/crm");
|
|
1458
1762
|
var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
|
|
1459
1763
|
function personInputFromApp(chapter, app) {
|
|
1460
1764
|
const input = sharedPersonInput({
|
|
@@ -1494,16 +1798,16 @@ async function syncApplicationToCrm(deps, opts) {
|
|
|
1494
1798
|
let recordId;
|
|
1495
1799
|
if (existing && typeof existing.id === "string") {
|
|
1496
1800
|
recordId = existing.id;
|
|
1497
|
-
await (0,
|
|
1801
|
+
await (0, import_crm5.updateRecord)(recordDeps, { id: recordId, input });
|
|
1498
1802
|
} else {
|
|
1499
|
-
const created = await (0,
|
|
1803
|
+
const created = await (0, import_crm5.createRecord)(recordDeps, { type: "person", input, ...stage ? { stage } : {} });
|
|
1500
1804
|
recordId = created.id;
|
|
1501
1805
|
}
|
|
1502
1806
|
if (existing && stage && existing.stage !== stage) {
|
|
1503
|
-
await (0,
|
|
1807
|
+
await (0, import_crm5.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
|
|
1504
1808
|
}
|
|
1505
1809
|
await deps.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
|
|
1506
|
-
await (0,
|
|
1810
|
+
await (0, import_crm5.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
|
|
1507
1811
|
return recordId;
|
|
1508
1812
|
}
|
|
1509
1813
|
async function backfillCrm(deps) {
|