@open-mercato/shared 0.7.1-develop.7175.1.d49ab48ee2 → 0.7.1-develop.7177.1.9827d081d4
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/.turbo/turbo-build.log +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/phone_calls/index.js +3 -0
- package/dist/modules/phone_calls/index.js.map +7 -0
- package/dist/modules/phone_calls/provider.js +35 -0
- package/dist/modules/phone_calls/provider.js.map +7 -0
- package/dist/modules/phone_calls/types.js +5 -0
- package/dist/modules/phone_calls/types.js.map +7 -0
- package/package.json +2 -2
- package/src/modules/phone_calls/__tests__/provider.test.ts +67 -0
- package/src/modules/phone_calls/index.ts +2 -0
- package/src/modules/phone_calls/provider.ts +50 -0
- package/src/modules/phone_calls/types.ts +93 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:shared] found
|
|
1
|
+
[build:shared] found 281 entry points
|
|
2
2
|
[build:shared] built successfully
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.7.1-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.7.1-develop.7177.1.9827d081d4';\nexport const appVersion = APP_VERSION;\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const PROVIDER_REGISTRY_KEY = "__openMercatoPhoneCallProviders__";
|
|
2
|
+
function getRegistry() {
|
|
3
|
+
const globalState = globalThis;
|
|
4
|
+
if (!globalState[PROVIDER_REGISTRY_KEY]) {
|
|
5
|
+
globalState[PROVIDER_REGISTRY_KEY] = /* @__PURE__ */ new Map();
|
|
6
|
+
}
|
|
7
|
+
return globalState[PROVIDER_REGISTRY_KEY];
|
|
8
|
+
}
|
|
9
|
+
function registerPhoneCallProvider(adapter) {
|
|
10
|
+
const registry = getRegistry();
|
|
11
|
+
registry.set(adapter.providerKey, adapter);
|
|
12
|
+
return () => {
|
|
13
|
+
if (registry.get(adapter.providerKey) === adapter) {
|
|
14
|
+
registry.delete(adapter.providerKey);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function getPhoneCallProvider(providerKey) {
|
|
19
|
+
return getRegistry().get(providerKey);
|
|
20
|
+
}
|
|
21
|
+
function listPhoneCallProviders() {
|
|
22
|
+
return Array.from(getRegistry().values()).sort(
|
|
23
|
+
(left, right) => left.displayName.localeCompare(right.displayName)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
function clearPhoneCallProviders() {
|
|
27
|
+
getRegistry().clear();
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
clearPhoneCallProviders,
|
|
31
|
+
getPhoneCallProvider,
|
|
32
|
+
listPhoneCallProviders,
|
|
33
|
+
registerPhoneCallProvider
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/phone_calls/provider.ts"],
|
|
4
|
+
"sourcesContent": ["import type {\n FetchPhoneCallsInput,\n NormalizedPhoneCallBatch,\n ProviderValidationResult,\n ValidatePhoneCallProviderInput,\n} from './types'\n\nexport interface PhoneCallProviderAdapter {\n readonly providerKey: string\n readonly displayName: string\n\n validateConnection(input: ValidatePhoneCallProviderInput): Promise<ProviderValidationResult>\n fetchCalls(input: FetchPhoneCallsInput): Promise<NormalizedPhoneCallBatch>\n}\n\nconst PROVIDER_REGISTRY_KEY = '__openMercatoPhoneCallProviders__'\n\nfunction getRegistry(): Map<string, PhoneCallProviderAdapter> {\n const globalState = globalThis as typeof globalThis & {\n [PROVIDER_REGISTRY_KEY]?: Map<string, PhoneCallProviderAdapter>\n }\n if (!globalState[PROVIDER_REGISTRY_KEY]) {\n globalState[PROVIDER_REGISTRY_KEY] = new Map<string, PhoneCallProviderAdapter>()\n }\n return globalState[PROVIDER_REGISTRY_KEY]\n}\n\nexport function registerPhoneCallProvider(adapter: PhoneCallProviderAdapter): () => void {\n const registry = getRegistry()\n registry.set(adapter.providerKey, adapter)\n return () => {\n if (registry.get(adapter.providerKey) === adapter) {\n registry.delete(adapter.providerKey)\n }\n }\n}\n\nexport function getPhoneCallProvider(providerKey: string): PhoneCallProviderAdapter | undefined {\n return getRegistry().get(providerKey)\n}\n\nexport function listPhoneCallProviders(): PhoneCallProviderAdapter[] {\n return Array.from(getRegistry().values()).sort((left, right) =>\n left.displayName.localeCompare(right.displayName),\n )\n}\n\nexport function clearPhoneCallProviders(): void {\n getRegistry().clear()\n}\n"],
|
|
5
|
+
"mappings": "AAeA,MAAM,wBAAwB;AAE9B,SAAS,cAAqD;AAC5D,QAAM,cAAc;AAGpB,MAAI,CAAC,YAAY,qBAAqB,GAAG;AACvC,gBAAY,qBAAqB,IAAI,oBAAI,IAAsC;AAAA,EACjF;AACA,SAAO,YAAY,qBAAqB;AAC1C;AAEO,SAAS,0BAA0B,SAA+C;AACvF,QAAM,WAAW,YAAY;AAC7B,WAAS,IAAI,QAAQ,aAAa,OAAO;AACzC,SAAO,MAAM;AACX,QAAI,SAAS,IAAI,QAAQ,WAAW,MAAM,SAAS;AACjD,eAAS,OAAO,QAAQ,WAAW;AAAA,IACrC;AAAA,EACF;AACF;AAEO,SAAS,qBAAqB,aAA2D;AAC9F,SAAO,YAAY,EAAE,IAAI,WAAW;AACtC;AAEO,SAAS,yBAAqD;AACnE,SAAO,MAAM,KAAK,YAAY,EAAE,OAAO,CAAC,EAAE;AAAA,IAAK,CAAC,MAAM,UACpD,KAAK,YAAY,cAAc,MAAM,WAAW;AAAA,EAClD;AACF;AAEO,SAAS,0BAAgC;AAC9C,cAAY,EAAE,MAAM;AACtB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/phone_calls/types.ts"],
|
|
4
|
+
"sourcesContent": ["export type PhoneCallDirection = 'inbound' | 'outbound' | 'internal' | 'unknown'\n\nexport type PhoneCallStatus =\n | 'new'\n | 'ringing'\n | 'answered'\n | 'missed'\n | 'failed'\n | 'completed'\n | 'unknown'\n\nexport type PhoneCallParticipantRole = 'caller' | 'callee' | 'agent' | 'unknown'\n\nexport interface NormalizedPhoneCallParticipant {\n role: PhoneCallParticipantRole\n providerParticipantId?: string | null\n phoneNumber?: string | null\n displayName?: string | null\n email?: string | null\n metadata?: Record<string, unknown>\n}\n\nexport interface NormalizedPhoneCallRecording {\n url?: string | null\n providerRecordingId?: string | null\n mimeType?: string | null\n durationSeconds?: number | null\n metadata?: Record<string, unknown>\n}\n\nexport interface NormalizedPhoneCall {\n externalCallId: string\n externalConversationId?: string | null\n direction: PhoneCallDirection\n status: PhoneCallStatus\n participants: NormalizedPhoneCallParticipant[]\n recording?: NormalizedPhoneCallRecording | null\n startedAt?: Date | null\n answeredAt?: Date | null\n endedAt?: Date | null\n durationSeconds?: number | null\n providerFacts?: Record<string, unknown>\n rawPayload: Record<string, unknown>\n}\n\nexport interface PhoneCallProviderScope {\n tenantId: string\n organizationId: string\n}\n\n// Declared here rather than next to the ingest command so client surfaces can read it without\n// pulling the command module (and MikroORM with it) into the browser bundle.\nexport const PHONE_CALL_RESOURCE_KIND = 'phone_calls.phone_call'\n\nexport type PhoneCallProviderCredentials = Record<string, unknown>\n\nexport interface ValidatePhoneCallProviderInput {\n credentials: PhoneCallProviderCredentials\n scope: PhoneCallProviderScope\n integrationId?: string | null\n}\n\nexport interface ProviderValidationResult {\n ok: boolean\n message?: string\n details?: Record<string, unknown>\n}\n\nexport interface FetchPhoneCallsInput {\n credentials: PhoneCallProviderCredentials\n scope: PhoneCallProviderScope\n integrationId?: string | null\n from?: Date | null\n to?: Date | null\n cursor?: string | null\n limit?: number | null\n}\n\n/**\n * Why a provider stopped walking its pages before the range was exhausted. Without this an\n * adapter that gave up on a broken response is indistinguishable from one that reached the\n * end, and the caller reports a partial sweep as a complete one.\n */\nexport type PhoneCallBatchAnomaly = 'page_not_echoed' | 'invalid_page_count' | 'empty_page'\n\nexport interface NormalizedPhoneCallBatch {\n calls: NormalizedPhoneCall[]\n nextCursor?: string | null\n /** Present only when the walk stopped on a response the adapter could not trust. */\n anomaly?: PhoneCallBatchAnomaly | null\n /** The cursor the anomalous page was requested with, so a retry can resume from it. */\n anomalyCursor?: string | null\n}\n"],
|
|
5
|
+
"mappings": "AAoDO,MAAM,2BAA2B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7177.1.9827d081d4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"@mikro-orm/core": "^7.1.14",
|
|
114
114
|
"@mikro-orm/decorators": "^7.1.14",
|
|
115
115
|
"@mikro-orm/postgresql": "^7.1.14",
|
|
116
|
-
"@open-mercato/cache": "0.7.1-develop.
|
|
116
|
+
"@open-mercato/cache": "0.7.1-develop.7177.1.9827d081d4",
|
|
117
117
|
"@types/html-to-text": "^9.0.4",
|
|
118
118
|
"@types/sanitize-html": "^2.16.1",
|
|
119
119
|
"dotenv": "^17.4.2",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearPhoneCallProviders,
|
|
3
|
+
getPhoneCallProvider,
|
|
4
|
+
listPhoneCallProviders,
|
|
5
|
+
registerPhoneCallProvider,
|
|
6
|
+
type PhoneCallProviderAdapter,
|
|
7
|
+
} from '../provider'
|
|
8
|
+
|
|
9
|
+
function adapter(providerKey: string, displayName: string): PhoneCallProviderAdapter {
|
|
10
|
+
return {
|
|
11
|
+
providerKey,
|
|
12
|
+
displayName,
|
|
13
|
+
validateConnection: async () => ({ ok: true }),
|
|
14
|
+
fetchCalls: async () => ({ calls: [], hasMore: false, nextCursor: null }),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('phone call provider registration', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
clearPhoneCallProviders()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('resolves a registered adapter by its provider key', () => {
|
|
24
|
+
const tillio = adapter('tillio', 'Tillio')
|
|
25
|
+
registerPhoneCallProvider(tillio)
|
|
26
|
+
|
|
27
|
+
expect(getPhoneCallProvider('tillio')).toBe(tillio)
|
|
28
|
+
expect(getPhoneCallProvider('absent')).toBeUndefined()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('keeps one entry per provider key when a second adapter claims it', () => {
|
|
32
|
+
registerPhoneCallProvider(adapter('tillio', 'Tillio'))
|
|
33
|
+
const replacement = adapter('tillio', 'Tillio v2')
|
|
34
|
+
registerPhoneCallProvider(replacement)
|
|
35
|
+
|
|
36
|
+
expect(getPhoneCallProvider('tillio')).toBe(replacement)
|
|
37
|
+
expect(listPhoneCallProviders()).toHaveLength(1)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('lists adapters by display name rather than registration order', () => {
|
|
41
|
+
registerPhoneCallProvider(adapter('zeta', 'Zeta Telecom'))
|
|
42
|
+
registerPhoneCallProvider(adapter('alpha', 'Alpha Voice'))
|
|
43
|
+
|
|
44
|
+
expect(listPhoneCallProviders().map((entry) => entry.providerKey)).toEqual(['alpha', 'zeta'])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('removes the adapter when its own disposer runs', () => {
|
|
48
|
+
const dispose = registerPhoneCallProvider(adapter('tillio', 'Tillio'))
|
|
49
|
+
dispose()
|
|
50
|
+
|
|
51
|
+
expect(getPhoneCallProvider('tillio')).toBeUndefined()
|
|
52
|
+
expect(listPhoneCallProviders()).toEqual([])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// A later registration takes the key from under the earlier disposer, which the caller
|
|
56
|
+
// may still hold. Deleting by key alone would let it unregister the adapter that
|
|
57
|
+
// replaced it.
|
|
58
|
+
it('ignores a disposer whose adapter was already replaced', () => {
|
|
59
|
+
const disposeFirst = registerPhoneCallProvider(adapter('tillio', 'Tillio'))
|
|
60
|
+
const replacement = adapter('tillio', 'Tillio v2')
|
|
61
|
+
registerPhoneCallProvider(replacement)
|
|
62
|
+
|
|
63
|
+
disposeFirst()
|
|
64
|
+
|
|
65
|
+
expect(getPhoneCallProvider('tillio')).toBe(replacement)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FetchPhoneCallsInput,
|
|
3
|
+
NormalizedPhoneCallBatch,
|
|
4
|
+
ProviderValidationResult,
|
|
5
|
+
ValidatePhoneCallProviderInput,
|
|
6
|
+
} from './types'
|
|
7
|
+
|
|
8
|
+
export interface PhoneCallProviderAdapter {
|
|
9
|
+
readonly providerKey: string
|
|
10
|
+
readonly displayName: string
|
|
11
|
+
|
|
12
|
+
validateConnection(input: ValidatePhoneCallProviderInput): Promise<ProviderValidationResult>
|
|
13
|
+
fetchCalls(input: FetchPhoneCallsInput): Promise<NormalizedPhoneCallBatch>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const PROVIDER_REGISTRY_KEY = '__openMercatoPhoneCallProviders__'
|
|
17
|
+
|
|
18
|
+
function getRegistry(): Map<string, PhoneCallProviderAdapter> {
|
|
19
|
+
const globalState = globalThis as typeof globalThis & {
|
|
20
|
+
[PROVIDER_REGISTRY_KEY]?: Map<string, PhoneCallProviderAdapter>
|
|
21
|
+
}
|
|
22
|
+
if (!globalState[PROVIDER_REGISTRY_KEY]) {
|
|
23
|
+
globalState[PROVIDER_REGISTRY_KEY] = new Map<string, PhoneCallProviderAdapter>()
|
|
24
|
+
}
|
|
25
|
+
return globalState[PROVIDER_REGISTRY_KEY]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function registerPhoneCallProvider(adapter: PhoneCallProviderAdapter): () => void {
|
|
29
|
+
const registry = getRegistry()
|
|
30
|
+
registry.set(adapter.providerKey, adapter)
|
|
31
|
+
return () => {
|
|
32
|
+
if (registry.get(adapter.providerKey) === adapter) {
|
|
33
|
+
registry.delete(adapter.providerKey)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getPhoneCallProvider(providerKey: string): PhoneCallProviderAdapter | undefined {
|
|
39
|
+
return getRegistry().get(providerKey)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function listPhoneCallProviders(): PhoneCallProviderAdapter[] {
|
|
43
|
+
return Array.from(getRegistry().values()).sort((left, right) =>
|
|
44
|
+
left.displayName.localeCompare(right.displayName),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function clearPhoneCallProviders(): void {
|
|
49
|
+
getRegistry().clear()
|
|
50
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export type PhoneCallDirection = 'inbound' | 'outbound' | 'internal' | 'unknown'
|
|
2
|
+
|
|
3
|
+
export type PhoneCallStatus =
|
|
4
|
+
| 'new'
|
|
5
|
+
| 'ringing'
|
|
6
|
+
| 'answered'
|
|
7
|
+
| 'missed'
|
|
8
|
+
| 'failed'
|
|
9
|
+
| 'completed'
|
|
10
|
+
| 'unknown'
|
|
11
|
+
|
|
12
|
+
export type PhoneCallParticipantRole = 'caller' | 'callee' | 'agent' | 'unknown'
|
|
13
|
+
|
|
14
|
+
export interface NormalizedPhoneCallParticipant {
|
|
15
|
+
role: PhoneCallParticipantRole
|
|
16
|
+
providerParticipantId?: string | null
|
|
17
|
+
phoneNumber?: string | null
|
|
18
|
+
displayName?: string | null
|
|
19
|
+
email?: string | null
|
|
20
|
+
metadata?: Record<string, unknown>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface NormalizedPhoneCallRecording {
|
|
24
|
+
url?: string | null
|
|
25
|
+
providerRecordingId?: string | null
|
|
26
|
+
mimeType?: string | null
|
|
27
|
+
durationSeconds?: number | null
|
|
28
|
+
metadata?: Record<string, unknown>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface NormalizedPhoneCall {
|
|
32
|
+
externalCallId: string
|
|
33
|
+
externalConversationId?: string | null
|
|
34
|
+
direction: PhoneCallDirection
|
|
35
|
+
status: PhoneCallStatus
|
|
36
|
+
participants: NormalizedPhoneCallParticipant[]
|
|
37
|
+
recording?: NormalizedPhoneCallRecording | null
|
|
38
|
+
startedAt?: Date | null
|
|
39
|
+
answeredAt?: Date | null
|
|
40
|
+
endedAt?: Date | null
|
|
41
|
+
durationSeconds?: number | null
|
|
42
|
+
providerFacts?: Record<string, unknown>
|
|
43
|
+
rawPayload: Record<string, unknown>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PhoneCallProviderScope {
|
|
47
|
+
tenantId: string
|
|
48
|
+
organizationId: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Declared here rather than next to the ingest command so client surfaces can read it without
|
|
52
|
+
// pulling the command module (and MikroORM with it) into the browser bundle.
|
|
53
|
+
export const PHONE_CALL_RESOURCE_KIND = 'phone_calls.phone_call'
|
|
54
|
+
|
|
55
|
+
export type PhoneCallProviderCredentials = Record<string, unknown>
|
|
56
|
+
|
|
57
|
+
export interface ValidatePhoneCallProviderInput {
|
|
58
|
+
credentials: PhoneCallProviderCredentials
|
|
59
|
+
scope: PhoneCallProviderScope
|
|
60
|
+
integrationId?: string | null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ProviderValidationResult {
|
|
64
|
+
ok: boolean
|
|
65
|
+
message?: string
|
|
66
|
+
details?: Record<string, unknown>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface FetchPhoneCallsInput {
|
|
70
|
+
credentials: PhoneCallProviderCredentials
|
|
71
|
+
scope: PhoneCallProviderScope
|
|
72
|
+
integrationId?: string | null
|
|
73
|
+
from?: Date | null
|
|
74
|
+
to?: Date | null
|
|
75
|
+
cursor?: string | null
|
|
76
|
+
limit?: number | null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Why a provider stopped walking its pages before the range was exhausted. Without this an
|
|
81
|
+
* adapter that gave up on a broken response is indistinguishable from one that reached the
|
|
82
|
+
* end, and the caller reports a partial sweep as a complete one.
|
|
83
|
+
*/
|
|
84
|
+
export type PhoneCallBatchAnomaly = 'page_not_echoed' | 'invalid_page_count' | 'empty_page'
|
|
85
|
+
|
|
86
|
+
export interface NormalizedPhoneCallBatch {
|
|
87
|
+
calls: NormalizedPhoneCall[]
|
|
88
|
+
nextCursor?: string | null
|
|
89
|
+
/** Present only when the walk stopped on a response the adapter could not trust. */
|
|
90
|
+
anomaly?: PhoneCallBatchAnomaly | null
|
|
91
|
+
/** The cursor the anomalous page was requested with, so a retry can resume from it. */
|
|
92
|
+
anomalyCursor?: string | null
|
|
93
|
+
}
|