@sinio/sdk 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +117 -2
- package/dist/cjs/client.d.ts +69 -0
- package/dist/cjs/client.js +176 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/errors.d.ts +34 -0
- package/dist/cjs/errors.js +60 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/http.d.ts +22 -0
- package/dist/cjs/http.js +139 -0
- package/dist/cjs/http.js.map +1 -0
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.js +18 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/serialize.d.ts +11 -0
- package/dist/cjs/serialize.js +111 -0
- package/dist/cjs/serialize.js.map +1 -0
- package/dist/cjs/types.d.ts +114 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/esm/client.d.ts +69 -0
- package/dist/esm/client.js +172 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/errors.d.ts +34 -0
- package/dist/esm/errors.js +49 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/http.d.ts +22 -0
- package/dist/esm/http.js +134 -0
- package/dist/esm/http.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/serialize.d.ts +11 -0
- package/dist/esm/serialize.js +102 -0
- package/dist/esm/serialize.js.map +1 -0
- package/dist/esm/types.d.ts +114 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/package.json +37 -4
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type FetchLike = (input: string, init: RequestInit) => Promise<Response>;
|
|
2
|
+
export interface TransportOptions {
|
|
3
|
+
readonly baseUrl: string;
|
|
4
|
+
readonly timeoutMs: number;
|
|
5
|
+
readonly maxRetries: number;
|
|
6
|
+
readonly retryBaseDelayMs: number;
|
|
7
|
+
readonly userAgent: string;
|
|
8
|
+
readonly fetch?: FetchLike;
|
|
9
|
+
/** Injected in tests so retries do not actually wait. */
|
|
10
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/** `Retry-After` is either a delay in seconds or an HTTP date. Both are accepted. */
|
|
13
|
+
export declare function parseRetryAfter(header: string | null, now?: number): number | null;
|
|
14
|
+
export declare class Transport {
|
|
15
|
+
private readonly options;
|
|
16
|
+
private readonly fetchImpl;
|
|
17
|
+
private readonly sleep;
|
|
18
|
+
constructor(options: TransportOptions);
|
|
19
|
+
post(path: string, body: unknown, headers: Record<string, string>): Promise<unknown>;
|
|
20
|
+
/** Exponential backoff with full jitter, unless the server told us how long to wait. */
|
|
21
|
+
private backoffMs;
|
|
22
|
+
}
|
package/dist/esm/http.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { SinioAuthError, SinioConnectionError, SinioRateLimitError, SinioRequestError, SinioServerError, } from "./errors.js";
|
|
2
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3
|
+
/** `Retry-After` is either a delay in seconds or an HTTP date. Both are accepted. */
|
|
4
|
+
export function parseRetryAfter(header, now = Date.now()) {
|
|
5
|
+
if (header === null) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const seconds = Number(header);
|
|
9
|
+
if (Number.isFinite(seconds)) {
|
|
10
|
+
return Math.max(0, seconds);
|
|
11
|
+
}
|
|
12
|
+
const date = Date.parse(header);
|
|
13
|
+
if (Number.isNaN(date)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return Math.max(0, (date - now) / 1000);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A 429 or a 5xx is retried, and so is a dropped connection: ingestion is
|
|
20
|
+
* idempotent, so a replay can only ever be counted as a duplicate. A 4xx that
|
|
21
|
+
* is not a 429 is the caller's fault and is raised immediately.
|
|
22
|
+
*/
|
|
23
|
+
function isRetriableStatus(status) {
|
|
24
|
+
return status === 429 || status >= 500;
|
|
25
|
+
}
|
|
26
|
+
export class Transport {
|
|
27
|
+
options;
|
|
28
|
+
fetchImpl;
|
|
29
|
+
sleep;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
const provided = options.fetch ?? globalThis.fetch;
|
|
33
|
+
if (provided === undefined) {
|
|
34
|
+
throw new Error("No global fetch available; pass `fetch` in the client options (Node < 18).");
|
|
35
|
+
}
|
|
36
|
+
this.fetchImpl = provided;
|
|
37
|
+
this.sleep = options.sleep ?? defaultSleep;
|
|
38
|
+
}
|
|
39
|
+
async post(path, body, headers) {
|
|
40
|
+
const url = `${this.options.baseUrl.replace(/\/+$/, "")}${path}`;
|
|
41
|
+
const payload = JSON.stringify(body);
|
|
42
|
+
let attempt = 0;
|
|
43
|
+
for (;;) {
|
|
44
|
+
let response;
|
|
45
|
+
try {
|
|
46
|
+
response = await this.fetchImpl(url, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: {
|
|
49
|
+
"content-type": "application/json",
|
|
50
|
+
accept: "application/json",
|
|
51
|
+
"user-agent": this.options.userAgent,
|
|
52
|
+
...headers,
|
|
53
|
+
},
|
|
54
|
+
body: payload,
|
|
55
|
+
signal: AbortSignal.timeout(this.options.timeoutMs),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (cause) {
|
|
59
|
+
if (attempt >= this.options.maxRetries) {
|
|
60
|
+
throw new SinioConnectionError(`Request to ${url} failed: ${String(cause)}`, cause);
|
|
61
|
+
}
|
|
62
|
+
await this.sleep(this.backoffMs(attempt, null));
|
|
63
|
+
attempt += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (response.ok) {
|
|
67
|
+
return await readJson(response);
|
|
68
|
+
}
|
|
69
|
+
const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
|
|
70
|
+
if (isRetriableStatus(response.status) && attempt < this.options.maxRetries) {
|
|
71
|
+
await this.sleep(this.backoffMs(attempt, retryAfter));
|
|
72
|
+
attempt += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
throw await toApiError(response, retryAfter);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Exponential backoff with full jitter, unless the server told us how long to wait. */
|
|
79
|
+
backoffMs(attempt, retryAfterSeconds) {
|
|
80
|
+
if (retryAfterSeconds !== null) {
|
|
81
|
+
return retryAfterSeconds * 1000;
|
|
82
|
+
}
|
|
83
|
+
const ceiling = Math.min(this.options.retryBaseDelayMs * 2 ** attempt, 30_000);
|
|
84
|
+
return Math.random() * ceiling;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function readJson(response) {
|
|
88
|
+
const text = await response.text();
|
|
89
|
+
if (text.length === 0) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(text);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return text;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function toApiError(response, retryAfterSeconds) {
|
|
100
|
+
const body = await readJson(response);
|
|
101
|
+
const code = extractCode(body);
|
|
102
|
+
const message = `${response.status} ${response.statusText}: ${extractMessage(body) ?? "no message"}`;
|
|
103
|
+
if (response.status === 401 || response.status === 403) {
|
|
104
|
+
return new SinioAuthError(message, response.status, body, code);
|
|
105
|
+
}
|
|
106
|
+
if (response.status === 429) {
|
|
107
|
+
return new SinioRateLimitError(message, response.status, body, retryAfterSeconds);
|
|
108
|
+
}
|
|
109
|
+
if (response.status >= 500) {
|
|
110
|
+
return new SinioServerError(message, response.status, body, code);
|
|
111
|
+
}
|
|
112
|
+
return new SinioRequestError(message, response.status, body, code);
|
|
113
|
+
}
|
|
114
|
+
function extractCode(body) {
|
|
115
|
+
if (typeof body === "object" && body !== null && "code" in body && typeof body.code === "string") {
|
|
116
|
+
return body.code;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
function extractMessage(body) {
|
|
121
|
+
if (typeof body === "string") {
|
|
122
|
+
return body;
|
|
123
|
+
}
|
|
124
|
+
if (typeof body !== "object" || body === null || !("message" in body)) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const { message } = body;
|
|
128
|
+
// NestJS's ValidationPipe returns an array of messages, one per failed rule.
|
|
129
|
+
if (Array.isArray(message)) {
|
|
130
|
+
return message.join("; ");
|
|
131
|
+
}
|
|
132
|
+
return typeof message === "string" ? message : null;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { SinioClient, SDK_VERSION, type SinioClientOptions } from "./client.ts";
|
|
2
|
+
export { SinioApiError, SinioAuthError, SinioConnectionError, SinioError, SinioRateLimitError, SinioRequestError, SinioServerError, SinioValidationError, } from "./errors.ts";
|
|
3
|
+
export { MAX_ITEMS_PER_CALL } from "./serialize.ts";
|
|
4
|
+
export type * from "./types.ts";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { SinioClient, SDK_VERSION } from "./client.js";
|
|
2
|
+
export { SinioApiError, SinioAuthError, SinioConnectionError, SinioError, SinioRateLimitError, SinioRequestError, SinioServerError, SinioValidationError, } from "./errors.js";
|
|
3
|
+
export { MAX_ITEMS_PER_CALL } from "./serialize.js";
|
|
4
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,WAAW,EAA2B,MAAM,aAAa,CAAC;AAChF,OAAO,EACL,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,UAAU,EACV,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AssignmentInput, EntityInput, LocationInput, SinioEvent, Timestamp } from "./types.ts";
|
|
2
|
+
/** The API caps every collection at 500 items per call. */
|
|
3
|
+
export declare const MAX_ITEMS_PER_CALL = 500;
|
|
4
|
+
/** The most units one event may stand for. */
|
|
5
|
+
export declare const MAX_EVENT_COUNT = 1000000;
|
|
6
|
+
export declare function toIso(value: Timestamp, field: string): string;
|
|
7
|
+
export declare function chunk<T>(items: readonly T[], size: number): T[][];
|
|
8
|
+
export declare function toWireEvent(event: SinioEvent, index: number): Record<string, unknown>;
|
|
9
|
+
export declare function toWireEntity(entity: EntityInput, index: number): Record<string, unknown>;
|
|
10
|
+
export declare function toWireLocation(location: LocationInput, index: number): Record<string, unknown>;
|
|
11
|
+
export declare function toWireAssignment(assignment: AssignmentInput, index: number): Record<string, unknown>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { SinioValidationError } from "./errors.js";
|
|
2
|
+
/** The API caps every collection at 500 items per call. */
|
|
3
|
+
export const MAX_ITEMS_PER_CALL = 500;
|
|
4
|
+
/** The most units one event may stand for. */
|
|
5
|
+
export const MAX_EVENT_COUNT = 1_000_000;
|
|
6
|
+
// The API's own rule: a `T`, seconds, an optional fraction, then `Z` or an offset. It rejects the
|
|
7
|
+
// event otherwise (`invalid_occurred_at`); refused here, it is a fixable local error before any call.
|
|
8
|
+
const ISO_WITH_OFFSET = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$/;
|
|
9
|
+
export function toIso(value, field) {
|
|
10
|
+
if (value instanceof Date) {
|
|
11
|
+
if (Number.isNaN(value.getTime())) {
|
|
12
|
+
throw new SinioValidationError(`${field} is an invalid Date`);
|
|
13
|
+
}
|
|
14
|
+
return value.toISOString();
|
|
15
|
+
}
|
|
16
|
+
if (typeof value !== "string" || !ISO_WITH_OFFSET.test(value)) {
|
|
17
|
+
throw new SinioValidationError(`${field} must be an ISO 8601 timestamp with an offset (got ${JSON.stringify(value)})`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
/** Drops `undefined` keys so they never reach the wire as `null`. */
|
|
22
|
+
function compact(record) {
|
|
23
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
|
|
24
|
+
}
|
|
25
|
+
export function chunk(items, size) {
|
|
26
|
+
const chunks = [];
|
|
27
|
+
for (let index = 0; index < items.length; index += size) {
|
|
28
|
+
chunks.push(items.slice(index, index + size));
|
|
29
|
+
}
|
|
30
|
+
return chunks;
|
|
31
|
+
}
|
|
32
|
+
export function toWireEvent(event, index) {
|
|
33
|
+
if (!event.sourceEventId) {
|
|
34
|
+
throw new SinioValidationError(`events[${index}].sourceEventId is required`);
|
|
35
|
+
}
|
|
36
|
+
if (!event.type) {
|
|
37
|
+
throw new SinioValidationError(`events[${index}].type is required`);
|
|
38
|
+
}
|
|
39
|
+
if (event.payload !== undefined &&
|
|
40
|
+
(typeof event.payload !== "object" || event.payload === null || Array.isArray(event.payload))) {
|
|
41
|
+
throw new SinioValidationError(`events[${index}].payload must be an object`);
|
|
42
|
+
}
|
|
43
|
+
if (event.count !== undefined && (!Number.isInteger(event.count) || event.count < 1 || event.count > MAX_EVENT_COUNT)) {
|
|
44
|
+
throw new SinioValidationError(`events[${index}].count must be an integer between 1 and ${MAX_EVENT_COUNT}`);
|
|
45
|
+
}
|
|
46
|
+
return compact({
|
|
47
|
+
source_event_id: event.sourceEventId,
|
|
48
|
+
source_version: event.sourceVersion,
|
|
49
|
+
project: event.project,
|
|
50
|
+
type: event.type,
|
|
51
|
+
label: event.label,
|
|
52
|
+
occurred_at: toIso(event.occurredAt, `events[${index}].occurredAt`),
|
|
53
|
+
roles: event.roles === undefined ? undefined : compact({ ...event.roles }),
|
|
54
|
+
payload: event.payload,
|
|
55
|
+
count: event.count,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function toWireEntity(entity, index) {
|
|
59
|
+
if (!entity.externalId) {
|
|
60
|
+
throw new SinioValidationError(`entities[${index}].externalId is required`);
|
|
61
|
+
}
|
|
62
|
+
if (!entity.kind) {
|
|
63
|
+
throw new SinioValidationError(`entities[${index}].kind is required`);
|
|
64
|
+
}
|
|
65
|
+
return compact({
|
|
66
|
+
external_id: entity.externalId,
|
|
67
|
+
kind: entity.kind,
|
|
68
|
+
subkind: entity.subkind,
|
|
69
|
+
label: entity.label,
|
|
70
|
+
location: entity.location,
|
|
71
|
+
attributes: entity.attributes,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function toWireLocation(location, index) {
|
|
75
|
+
if (!location.externalId) {
|
|
76
|
+
throw new SinioValidationError(`locations[${index}].externalId is required`);
|
|
77
|
+
}
|
|
78
|
+
if (!location.name) {
|
|
79
|
+
throw new SinioValidationError(`locations[${index}].name is required`);
|
|
80
|
+
}
|
|
81
|
+
return compact({
|
|
82
|
+
external_id: location.externalId,
|
|
83
|
+
kind: location.kind,
|
|
84
|
+
name: location.name,
|
|
85
|
+
parent: location.parent,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
export function toWireAssignment(assignment, index) {
|
|
89
|
+
if (!assignment.credential || !assignment.subject) {
|
|
90
|
+
throw new SinioValidationError(`assignments[${index}] requires both credential and subject`);
|
|
91
|
+
}
|
|
92
|
+
return compact({
|
|
93
|
+
credential: assignment.credential,
|
|
94
|
+
subject: assignment.subject,
|
|
95
|
+
relation: assignment.relation,
|
|
96
|
+
valid_from: toIso(assignment.validFrom, `assignments[${index}].validFrom`),
|
|
97
|
+
valid_to: assignment.validTo === undefined || assignment.validTo === null
|
|
98
|
+
? undefined
|
|
99
|
+
: toIso(assignment.validTo, `assignments[${index}].validTo`),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=serialize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialize.js","sourceRoot":"","sources":["../../src/serialize.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** Ingestion mode. `backfill` persists history without triggering alerts. */
|
|
2
|
+
export type IngestionMode = "webhook" | "backfill";
|
|
3
|
+
/** Kinds accepted by `POST /v1/entities`. */
|
|
4
|
+
export type EntityKind = "device" | "subject" | "asset" | "actor" | "credential";
|
|
5
|
+
/** Levels accepted by `POST /v1/locations`. */
|
|
6
|
+
export type LocationLevel = "site" | "zone" | "point";
|
|
7
|
+
/** An ISO 8601 timestamp *with* an offset, or a `Date`. */
|
|
8
|
+
export type Timestamp = string | Date;
|
|
9
|
+
/**
|
|
10
|
+
* The four structural roles the derivation engine resolves, each a raw
|
|
11
|
+
* identifier from the source. Anything else (ticket number, firmware, ...)
|
|
12
|
+
* belongs in `payload`. The API refuses any other key.
|
|
13
|
+
*/
|
|
14
|
+
export interface EventRoles {
|
|
15
|
+
readonly device?: string;
|
|
16
|
+
readonly subject?: string;
|
|
17
|
+
readonly location?: string;
|
|
18
|
+
readonly actor?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface SinioEvent {
|
|
21
|
+
/** Identity at the source, stable over time. Part of the idempotency key. */
|
|
22
|
+
readonly sourceEventId: string;
|
|
23
|
+
/** Source-side version, kept for tracing: it plays no part in deduplication. */
|
|
24
|
+
readonly sourceVersion?: string;
|
|
25
|
+
/** Project `external_id`. Required for publisher-level keys, forbidden for project-level ones. */
|
|
26
|
+
readonly project?: string;
|
|
27
|
+
/** `code` of an event type in the catalog. */
|
|
28
|
+
readonly type: string;
|
|
29
|
+
readonly label?: string;
|
|
30
|
+
/** ISO 8601 with offset. Without an offset the API rejects the event. */
|
|
31
|
+
readonly occurredAt: Timestamp;
|
|
32
|
+
readonly roles?: EventRoles;
|
|
33
|
+
/** Free-form, optional (`{}` when omitted); the readings (battery, signal, ...) are read from it as its source declares them. */
|
|
34
|
+
readonly payload?: Record<string, unknown>;
|
|
35
|
+
/** How many units this one event stands for (a button pressed ten times). An integer from 1 to 1,000,000; omitted = 1. */
|
|
36
|
+
readonly count?: number;
|
|
37
|
+
}
|
|
38
|
+
export interface EntityInput {
|
|
39
|
+
readonly externalId: string;
|
|
40
|
+
readonly kind: EntityKind;
|
|
41
|
+
readonly subkind?: string;
|
|
42
|
+
readonly label?: string;
|
|
43
|
+
/** External id of the location the entity sits in. */
|
|
44
|
+
readonly location?: string;
|
|
45
|
+
readonly attributes?: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
export interface LocationInput {
|
|
48
|
+
readonly externalId: string;
|
|
49
|
+
readonly kind: LocationLevel;
|
|
50
|
+
readonly name: string;
|
|
51
|
+
/** External id of the parent location, if any. */
|
|
52
|
+
readonly parent?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface AssignmentInput {
|
|
55
|
+
/** External id of the credential (badge, card, ...). */
|
|
56
|
+
readonly credential: string;
|
|
57
|
+
/** External id of the subject the credential is assigned to. */
|
|
58
|
+
readonly subject: string;
|
|
59
|
+
readonly relation?: string;
|
|
60
|
+
readonly validFrom: Timestamp;
|
|
61
|
+
readonly validTo?: Timestamp | null;
|
|
62
|
+
}
|
|
63
|
+
export interface EventRejection {
|
|
64
|
+
readonly sourceEventId: string;
|
|
65
|
+
readonly code: string;
|
|
66
|
+
readonly message: string;
|
|
67
|
+
}
|
|
68
|
+
export interface ReferenceRejection {
|
|
69
|
+
readonly externalId: string;
|
|
70
|
+
readonly code: string;
|
|
71
|
+
readonly message: string;
|
|
72
|
+
}
|
|
73
|
+
export interface AssignmentRejection {
|
|
74
|
+
readonly credential: string;
|
|
75
|
+
readonly subject: string;
|
|
76
|
+
readonly code: string;
|
|
77
|
+
readonly message: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Outcome of `sendEvents`. `duplicates` and `revisions` are successes, not
|
|
81
|
+
* errors: replaying a batch is expected and never creates anything twice.
|
|
82
|
+
*/
|
|
83
|
+
export interface SendEventsResult {
|
|
84
|
+
/** Server-side batch ids, one per HTTP call the SDK made (batches are chunked at 500). */
|
|
85
|
+
readonly batchIds: string[];
|
|
86
|
+
readonly accepted: number;
|
|
87
|
+
readonly pendingClassification: number;
|
|
88
|
+
readonly duplicates: number;
|
|
89
|
+
readonly revisions: number;
|
|
90
|
+
readonly rejected: number;
|
|
91
|
+
readonly rejections: EventRejection[];
|
|
92
|
+
/** The key is in test mode: its events are checked and kept apart, never used for detection. */
|
|
93
|
+
readonly testMode: boolean;
|
|
94
|
+
}
|
|
95
|
+
export interface UpsertResult {
|
|
96
|
+
readonly accepted: number;
|
|
97
|
+
readonly rejected: number;
|
|
98
|
+
readonly rejections: ReferenceRejection[];
|
|
99
|
+
}
|
|
100
|
+
export interface AssignmentsResult {
|
|
101
|
+
readonly accepted: number;
|
|
102
|
+
readonly rejected: number;
|
|
103
|
+
readonly rejections: AssignmentRejection[];
|
|
104
|
+
}
|
|
105
|
+
export interface SendEventsOptions {
|
|
106
|
+
/** Defaults to `webhook`. */
|
|
107
|
+
readonly mode?: IngestionMode;
|
|
108
|
+
/** Overrides the client-level source code for this call. */
|
|
109
|
+
readonly sourceCode?: string;
|
|
110
|
+
}
|
|
111
|
+
/** Project override, for publisher-level API keys covering several projects. */
|
|
112
|
+
export interface ProjectOptions {
|
|
113
|
+
readonly project?: string;
|
|
114
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sinio/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official Node.js SDK for the sinio.ai ingestion API (events, entities, locations, assignments).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/cjs/index.js",
|
|
8
|
+
"types": "./dist/esm/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/esm/index.d.ts",
|
|
12
|
+
"import": "./dist/esm/index.js",
|
|
13
|
+
"require": "./dist/cjs/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
|
|
26
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
27
|
+
"test": "tsc -p tsconfig.json --noEmit && node --test --experimental-strip-types test/*.test.ts"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.10.0",
|
|
31
|
+
"typescript": "^5.7.0"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"sinio",
|
|
35
|
+
"events",
|
|
36
|
+
"ingestion",
|
|
37
|
+
"sdk"
|
|
38
|
+
]
|
|
39
|
+
}
|