sizmo 0.4.1 → 0.5.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 +38 -0
- package/commands/crm.mjs +258 -0
- package/commands/noshow.mjs +46 -5
- package/commands/pipeline.mjs +87 -14
- package/commands/reconcile.mjs +46 -4
- package/commands/snapshot.mjs +67 -9
- package/commands/sync.mjs +84 -0
- package/docs/how-to/crm-model.md +105 -0
- package/lib/context.mjs +36 -1
- package/lib/model.mjs +213 -0
- package/lib/registry.mjs +3 -1
- package/lib/resolver.mjs +137 -0
- package/package.json +3 -3
package/lib/resolver.mjs
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// lib/resolver.mjs — id→name maps from the CRM model. Never fabricates a name.
|
|
2
|
+
// makeResolver(model, {now}) → resolver with .resolve(kind, id) and .label(kind, id).
|
|
3
|
+
//
|
|
4
|
+
// resolve(kind, id) → { name: string|null, status: 'hit'|'miss'|'stale', pipelineId?, pipelineName? }
|
|
5
|
+
// hit — found in model, entity is fresh
|
|
6
|
+
// stale — found in model, but entity is past its TTL
|
|
7
|
+
// miss — not found (never fabricated; name=null)
|
|
8
|
+
//
|
|
9
|
+
// label(kind, id) → string suitable for human display:
|
|
10
|
+
// hit/stale → name (stale callers should add age note separately)
|
|
11
|
+
// miss → '<unknown:<id> — run sizmo sync>'
|
|
12
|
+
//
|
|
13
|
+
// Kind mapping:
|
|
14
|
+
// 'pipeline' → pipelines entity (id→name)
|
|
15
|
+
// 'stage' → stages nested in pipelines (id→{name,pipelineId,pipelineName,position})
|
|
16
|
+
// 'calendar' → calendars entity
|
|
17
|
+
// 'tag' → tags entity
|
|
18
|
+
// 'customField' → customFields entity
|
|
19
|
+
// 'user' → users entity
|
|
20
|
+
import { isStale, ENTITY_SPECS } from './model.mjs';
|
|
21
|
+
|
|
22
|
+
const TTL_MAP = Object.fromEntries(ENTITY_SPECS.map(s => [s.name, s.ttlMs]));
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* makeResolver — build maps from a model blob; return resolver.
|
|
26
|
+
* @param {object|null} model the loaded model blob (or null if missing)
|
|
27
|
+
* @param {object} opts
|
|
28
|
+
* @param {Function} opts.now injectable clock () => ms
|
|
29
|
+
*/
|
|
30
|
+
export function makeResolver(model, { now = Date.now } = {}) {
|
|
31
|
+
// Materialized maps — built once on construction
|
|
32
|
+
const maps = buildMaps(model);
|
|
33
|
+
|
|
34
|
+
function resolveKind(kind, id) {
|
|
35
|
+
const { entityName, map } = kindMeta(kind, maps);
|
|
36
|
+
if (!map) return { name: null, status: 'miss' };
|
|
37
|
+
|
|
38
|
+
const entry = map.get(id);
|
|
39
|
+
if (!entry) return { name: null, status: 'miss' };
|
|
40
|
+
|
|
41
|
+
// Check staleness of the source entity
|
|
42
|
+
const entity = model?.entities?.[entityName];
|
|
43
|
+
const ttl = TTL_MAP[entityName];
|
|
44
|
+
const stale = entity && ttl ? isStale(entity, now(), ttl) : false;
|
|
45
|
+
|
|
46
|
+
const result = { name: entry.name, status: stale ? 'stale' : 'hit' };
|
|
47
|
+
if (entry.pipelineId !== undefined) result.pipelineId = entry.pipelineId;
|
|
48
|
+
if (entry.pipelineName !== undefined) result.pipelineName = entry.pipelineName;
|
|
49
|
+
if (entry.position !== undefined) result.position = entry.position;
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
/**
|
|
55
|
+
* resolve(kind, id) → { name, status, ...extras }
|
|
56
|
+
* Never throws; miss returns { name: null, status: 'miss' }.
|
|
57
|
+
*/
|
|
58
|
+
resolve(kind, id) {
|
|
59
|
+
try { return resolveKind(kind, id); }
|
|
60
|
+
catch { return { name: null, status: 'miss' }; }
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* label(kind, id) → human string.
|
|
65
|
+
* hit/stale → name; miss → '<unknown:<id> — run sizmo sync>'
|
|
66
|
+
*/
|
|
67
|
+
label(kind, id) {
|
|
68
|
+
const r = resolveKind(kind, id);
|
|
69
|
+
if (r.name !== null) return r.name;
|
|
70
|
+
return `<unknown:${id} — run sizmo sync>`;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── internal ──────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function buildMaps(model) {
|
|
78
|
+
const empty = () => new Map();
|
|
79
|
+
if (!model || !model.entities) {
|
|
80
|
+
return { pipeline: empty(), stage: empty(), calendar: empty(), tag: empty(), customField: empty(), user: empty() };
|
|
81
|
+
}
|
|
82
|
+
const e = model.entities;
|
|
83
|
+
|
|
84
|
+
// pipelines + stages
|
|
85
|
+
const pipelineMap = new Map();
|
|
86
|
+
const stageMap = new Map();
|
|
87
|
+
if (e.pipelines && !e.pipelines.blocked && Array.isArray(e.pipelines.items)) {
|
|
88
|
+
for (const pl of e.pipelines.items) {
|
|
89
|
+
pipelineMap.set(pl.id, { name: pl.name });
|
|
90
|
+
for (const s of (pl.stages || [])) {
|
|
91
|
+
stageMap.set(s.id, { name: s.name, pipelineId: pl.id, pipelineName: pl.name, position: s.position ?? 0 });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// calendars
|
|
97
|
+
const calMap = new Map();
|
|
98
|
+
if (e.calendars && !e.calendars.blocked && Array.isArray(e.calendars.items)) {
|
|
99
|
+
for (const c of e.calendars.items) calMap.set(c.id, { name: c.name });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// tags
|
|
103
|
+
const tagMap = new Map();
|
|
104
|
+
if (e.tags && !e.tags.blocked && Array.isArray(e.tags.items)) {
|
|
105
|
+
for (const t of e.tags.items) tagMap.set(t.id, { name: t.name });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// customFields
|
|
109
|
+
const fieldMap = new Map();
|
|
110
|
+
if (e.customFields && !e.customFields.blocked && Array.isArray(e.customFields.items)) {
|
|
111
|
+
for (const f of e.customFields.items) fieldMap.set(f.id, { name: f.name, key: f.fieldKey });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// users
|
|
115
|
+
const userMap = new Map();
|
|
116
|
+
if (e.users && !e.users.blocked && Array.isArray(e.users.items)) {
|
|
117
|
+
for (const u of e.users.items) {
|
|
118
|
+
const name = [u.firstName, u.lastName].filter(Boolean).join(' ') || u.name || u.email || u.id;
|
|
119
|
+
userMap.set(u.id, { name });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { pipeline: pipelineMap, stage: stageMap, calendar: calMap, tag: tagMap, customField: fieldMap, user: userMap };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Which entity-name and map corresponds to each public kind string
|
|
127
|
+
function kindMeta(kind, maps) {
|
|
128
|
+
switch (kind) {
|
|
129
|
+
case 'pipeline': return { entityName: 'pipelines', map: maps.pipeline };
|
|
130
|
+
case 'stage': return { entityName: 'pipelines', map: maps.stage };
|
|
131
|
+
case 'calendar': return { entityName: 'calendars', map: maps.calendar };
|
|
132
|
+
case 'tag': return { entityName: 'tags', map: maps.tag };
|
|
133
|
+
case 'customField': return { entityName: 'customFields', map: maps.customField };
|
|
134
|
+
case 'user': return { entityName: 'users', map: maps.user };
|
|
135
|
+
default: return { entityName: null, map: null };
|
|
136
|
+
}
|
|
137
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sizmo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Unofficial read-only GoHighLevel CLI — read your CRM (leads, bookings, pipeline, A/R, payments) from the terminal. Not affiliated with HighLevel.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"node": ">=20"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"test": "node --test",
|
|
14
|
-
"prepublishOnly": "node --test"
|
|
13
|
+
"test": "node --test --test-concurrency=1",
|
|
14
|
+
"prepublishOnly": "node --test --test-concurrency=1"
|
|
15
15
|
},
|
|
16
16
|
"author": "Sizmo / CJ Salamida",
|
|
17
17
|
"license": "MIT",
|