@xtellig/autosocializer 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Xtellig
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # @xtellig/autosocializer
2
+
3
+ Official SDK for the [**Autosocializer**](https://www.autosocializer.com) platform, by [Xtellig](https://www.xtellig.com).
4
+
5
+ Create SeamlessBoard records via GraphQL with a small, typed client — including multipart file attachments.
6
+
7
+ > MIT licensed · Node.js ≥ 18
8
+
9
+ ---
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @xtellig/autosocializer
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Quick start
20
+
21
+ ```ts
22
+ import { SeamlessBoardApi } from "@xtellig/autosocializer";
23
+
24
+ const api = new SeamlessBoardApi({
25
+ url: "https://xtellig.autosocializer.com/api/graphql-endpoint",
26
+ apiKey: "TOKEN",
27
+ });
28
+
29
+ const res = await api.addRecord({
30
+ section: "67a448a8c78d6c21598d67d5",
31
+ data: {
32
+ priority_select_jlaFEbose: "High",
33
+ gender_select_BgSd_OMKS: "Male",
34
+ marks_number_4LbncDrii: 83,
35
+ number_field_number_s6ECxMbr3: 74,
36
+ email_email_GOv6dM7VF: "Theodora.Pacocha14@xtellig.com",
37
+ due_date_date_g8DAgJjta: "2025-12-28T22:16:48.102Z",
38
+ },
39
+ });
40
+
41
+ console.log(res._id);
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Attachments / file fields
47
+
48
+ Attachment fields cannot be sent as plain JSON. Wrap file bytes with `file()` — the client switches to GraphQL multipart FormData automatically.
49
+
50
+ Content-type is optional. When omitted, it is inferred from the filename (fallback: `application/octet-stream`).
51
+
52
+ ```ts
53
+ import { readFileSync } from "node:fs";
54
+ import { SeamlessBoardApi, file } from "@xtellig/autosocializer";
55
+
56
+ const api = new SeamlessBoardApi({
57
+ url: "https://xtellig.autosocializer.com/api/graphql-endpoint",
58
+ apiKey: "TOKEN",
59
+ });
60
+
61
+ const resume = file(readFileSync("./cv.pdf"), "cv.pdf");
62
+ // or with an explicit MIME type:
63
+ // const resume = file(buffer, "cv.pdf", "application/pdf");
64
+
65
+ const res = await api.addRecord({
66
+ section: "67a448a8c78d6c21598d67d5",
67
+ data: {
68
+ email_email_GOv6dM7VF: "user@example.com",
69
+ resume_attachment_FIELD_ID: resume,
70
+ // multiple files on one field:
71
+ // documents_attachment_FIELD_ID: [file(buf1, "a.pdf"), file(buf2, "b.png")],
72
+ },
73
+ });
74
+ ```
75
+
76
+ `file()` accepts `Buffer`, `Uint8Array`, `ArrayBuffer`, or `Blob`.
77
+
78
+ ---
79
+
80
+ ## Server-only (this release)
81
+
82
+ `SeamlessBoardApi.addRecord` is **blocked in the browser**. Call it from Node.js (or another server runtime). If invoked in a browser it throws `BrowserEnvironmentError`.
83
+
84
+ Future package features may be browser-allowed; the environment guard is applied **per feature**, not globally.
85
+
86
+ ---
87
+
88
+ ## API reference
89
+
90
+ ### `new SeamlessBoardApi(config)`
91
+
92
+ | Param | Type | Description |
93
+ | --------------- | -------- | ---------------------------------------- |
94
+ | `config.url` | `string` | GraphQL endpoint URL |
95
+ | `config.apiKey` | `string` | Access token (`Authorization: Bearer …`) |
96
+
97
+ ### `api.addRecord(input)`
98
+
99
+ | Param | Type | Description |
100
+ | --------------- | ------------ | ------------------------------ |
101
+ | `input.section` | `string` | Board section ID |
102
+ | `input.data` | `RecordData` | Field values keyed by field ID |
103
+
104
+ **Returns:** `Promise<{ _id: string; no: number; }>`
105
+
106
+ ### `file(content, filename, contentType?)`
107
+
108
+ Marks binary content as an attachment for multipart upload.
109
+
110
+ ### Errors
111
+
112
+ | Class | When |
113
+ | ------------------------- | -------------------------------------------- |
114
+ | `ApiError` | HTTP or GraphQL failure (`status`, `errors`) |
115
+ | `BrowserEnvironmentError` | Server-only feature called in a browser |
116
+ | `AutosocializerError` | Base class for the above |
117
+
118
+ ```ts
119
+ import { SeamlessBoardApi, ApiError } from "@xtellig/autosocializer";
120
+
121
+ try {
122
+ await api.addRecord({
123
+ section: "…",
124
+ data: {
125
+ /* … */
126
+ },
127
+ });
128
+ } catch (err) {
129
+ if (err instanceof ApiError) {
130
+ console.error(err.status, err.errors);
131
+ } else {
132
+ throw err;
133
+ }
134
+ }
135
+ ```
136
+
137
+ ### TypeScript types
138
+
139
+ All types are exported for use in your own code:
140
+
141
+ | Type | Description |
142
+ | ------------------------ | --------------------------------------------- |
143
+ | `SeamlessBoardApiConfig` | Constructor config (`url`, `apiKey`) |
144
+ | `AddRecordInput` | `{ section, data }` passed to `addRecord` |
145
+ | `AddRecordResult` | `{ _id }` returned by `addRecord` |
146
+ | `RecordData` | Field values keyed by field ID |
147
+ | `RecordValue` | A scalar, one `FileUpload`, or `FileUpload[]` |
148
+ | `ScalarValue` | `string \| number \| boolean \| null` |
149
+ | `FileUpload` | Marker returned by `file()` |
150
+ | `FileContent` | Accepted input for `file()` content |
151
+
152
+ ---
153
+
154
+ ## Requirements
155
+
156
+ - Node.js **≥ 18** (uses global `fetch`, `FormData`, `Blob`)
157
+ - A valid Autosocializer **Access Token**
158
+
159
+ ---
160
+
161
+ ## License
162
+
163
+ [MIT](./LICENSE) © Xtellig
package/dist/index.cjs ADDED
@@ -0,0 +1,346 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ApiError: () => ApiError,
24
+ AutosocializerError: () => AutosocializerError,
25
+ BrowserEnvironmentError: () => BrowserEnvironmentError,
26
+ FileUpload: () => FileUpload,
27
+ SeamlessBoardApi: () => SeamlessBoardApi,
28
+ file: () => file
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/errors.ts
33
+ var AutosocializerError = class extends Error {
34
+ constructor(message) {
35
+ super(message);
36
+ this.name = "AutosocializerError";
37
+ }
38
+ };
39
+ var ApiError = class extends AutosocializerError {
40
+ /** HTTP status code when available. */
41
+ status;
42
+ /** Raw GraphQL error payload when available. */
43
+ errors;
44
+ constructor(message, options) {
45
+ super(message);
46
+ this.name = "ApiError";
47
+ this.status = options?.status;
48
+ this.errors = options?.errors;
49
+ }
50
+ };
51
+ var BrowserEnvironmentError = class extends AutosocializerError {
52
+ /** Feature name that was blocked (e.g. "SeamlessBoardApi.addRecord"). */
53
+ feature;
54
+ constructor(feature) {
55
+ super(
56
+ `${feature} is only available in a server environment (Node.js). It cannot run in the browser. Call it from your backend instead.`
57
+ );
58
+ this.name = "BrowserEnvironmentError";
59
+ this.feature = feature;
60
+ }
61
+ };
62
+
63
+ // src/env/guard.ts
64
+ function isBrowserEnvironment() {
65
+ return typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
66
+ }
67
+ function assertServerEnvironment(feature) {
68
+ if (isBrowserEnvironment()) {
69
+ throw new BrowserEnvironmentError(feature);
70
+ }
71
+ }
72
+
73
+ // src/graphql/mutation.ts
74
+ var ADD_RECORD_MUTATION = `
75
+ mutation addSeamlessBoardRecord($createRecordViaApiInput: CreateRecordViaApiInput!) {
76
+ addSeamlessBoardRecord(createRecordViaApiInput: $createRecordViaApiInput) {
77
+ _id
78
+ no
79
+ }
80
+ }
81
+ `.trim();
82
+ var ADD_RECORD_OPERATION_NAME = "addSeamlessBoardRecord";
83
+
84
+ // src/attachments/mime.ts
85
+ var MIME_BY_EXT = {
86
+ // Documents
87
+ pdf: "application/pdf",
88
+ doc: "application/msword",
89
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
90
+ xls: "application/vnd.ms-excel",
91
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
92
+ ppt: "application/vnd.ms-powerpoint",
93
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
94
+ txt: "text/plain",
95
+ csv: "text/csv",
96
+ json: "application/json",
97
+ xml: "application/xml",
98
+ // Images
99
+ png: "image/png",
100
+ jpg: "image/jpeg",
101
+ jpeg: "image/jpeg",
102
+ gif: "image/gif",
103
+ webp: "image/webp",
104
+ svg: "image/svg+xml",
105
+ ico: "image/x-icon",
106
+ bmp: "image/bmp",
107
+ // Audio / video
108
+ mp3: "audio/mpeg",
109
+ wav: "audio/wav",
110
+ ogg: "audio/ogg",
111
+ mp4: "video/mp4",
112
+ webm: "video/webm",
113
+ // Archives
114
+ zip: "application/zip",
115
+ rar: "application/vnd.rar",
116
+ "7z": "application/x-7z-compressed",
117
+ gz: "application/gzip",
118
+ tar: "application/x-tar"
119
+ };
120
+ function resolveMime(filename, contentType) {
121
+ if (contentType && contentType.trim()) {
122
+ return contentType.trim();
123
+ }
124
+ const ext = filename.split(".").pop()?.toLowerCase() ?? "";
125
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
126
+ }
127
+
128
+ // src/attachments/file.ts
129
+ var FileUpload = class {
130
+ content;
131
+ filename;
132
+ contentType;
133
+ /** @internal */
134
+ constructor(content, filename, contentType) {
135
+ this.content = content;
136
+ this.filename = filename;
137
+ this.contentType = contentType;
138
+ }
139
+ };
140
+ function file(content, filename, contentType) {
141
+ return new FileUpload(content, filename, resolveMime(filename, contentType));
142
+ }
143
+ function isFileUpload(value) {
144
+ return value instanceof FileUpload;
145
+ }
146
+
147
+ // src/graphql/multipart.ts
148
+ function dummyKey() {
149
+ return `dummy-${Math.random().toString(36).substring(2, 10)}`;
150
+ }
151
+ function toBlob(upload) {
152
+ if (upload.content instanceof Blob) {
153
+ return upload.content.type ? upload.content : new Blob([upload.content], { type: upload.contentType });
154
+ }
155
+ return new Blob([upload.content], {
156
+ type: upload.contentType
157
+ });
158
+ }
159
+ function transformField(value, fieldId, parts) {
160
+ if (isFileUpload(value)) {
161
+ const key = dummyKey();
162
+ parts.push({
163
+ key: String(parts.length + 1),
164
+ path: `variables.createRecordViaApiInput.data.${fieldId}.${key}`,
165
+ upload: value
166
+ });
167
+ return { [key]: null };
168
+ }
169
+ if (Array.isArray(value) && value.some(isFileUpload)) {
170
+ const obj = {};
171
+ for (const item of value) {
172
+ if (!isFileUpload(item)) continue;
173
+ const key = dummyKey();
174
+ parts.push({
175
+ key: String(parts.length + 1),
176
+ path: `variables.createRecordViaApiInput.data.${fieldId}.${key}`,
177
+ upload: item
178
+ });
179
+ obj[key] = null;
180
+ }
181
+ return obj;
182
+ }
183
+ return value;
184
+ }
185
+ function hasFileUploads(data) {
186
+ for (const value of Object.values(data)) {
187
+ if (isFileUpload(value)) return true;
188
+ if (Array.isArray(value) && value.some(isFileUpload)) return true;
189
+ }
190
+ return false;
191
+ }
192
+ function buildMultipartFormData(section, data) {
193
+ const parts = [];
194
+ const transformed = {};
195
+ for (const [fieldId, value] of Object.entries(data)) {
196
+ transformed[fieldId] = transformField(value, fieldId, parts);
197
+ }
198
+ const operations = {
199
+ query: ADD_RECORD_MUTATION,
200
+ variables: {
201
+ createRecordViaApiInput: {
202
+ section,
203
+ data: transformed
204
+ }
205
+ }
206
+ };
207
+ const map = {};
208
+ for (const part of parts) {
209
+ map[part.key] = [part.path];
210
+ }
211
+ const formData = new FormData();
212
+ formData.append("operations", JSON.stringify(operations));
213
+ formData.append("map", JSON.stringify(map));
214
+ for (const part of parts) {
215
+ formData.append(part.key, toBlob(part.upload), part.upload.filename);
216
+ }
217
+ return { formData, hasAttachments: parts.length > 0 };
218
+ }
219
+
220
+ // src/http/request.ts
221
+ async function graphqlRequest(options) {
222
+ const headers = {
223
+ Authorization: `Bearer ${options.apiKey}`,
224
+ "x-apollo-operation-name": options.operationName,
225
+ ...options.headers
226
+ };
227
+ if (typeof options.body === "string") {
228
+ headers["Content-Type"] = "application/json";
229
+ }
230
+ const response = await fetch(options.url, {
231
+ method: "POST",
232
+ headers,
233
+ body: options.body
234
+ });
235
+ let payload;
236
+ const text = await response.text();
237
+ try {
238
+ payload = text ? JSON.parse(text) : void 0;
239
+ } catch {
240
+ throw new ApiError(
241
+ `Invalid JSON response from GraphQL endpoint (HTTP ${response.status}).`,
242
+ { status: response.status }
243
+ );
244
+ }
245
+ if (!response.ok) {
246
+ const message = payload?.errors?.map((e) => e.message).filter(Boolean).join("; ") || `GraphQL request failed with HTTP ${response.status}.`;
247
+ throw new ApiError(message, {
248
+ status: response.status,
249
+ errors: payload?.errors
250
+ });
251
+ }
252
+ if (payload?.errors?.length) {
253
+ const message = payload.errors.map((e) => e.message).filter(Boolean).join("; ") || "GraphQL request returned errors.";
254
+ throw new ApiError(message, {
255
+ status: response.status,
256
+ errors: payload.errors
257
+ });
258
+ }
259
+ if (!payload?.data) {
260
+ throw new ApiError("GraphQL response contained no data.", {
261
+ status: response.status
262
+ });
263
+ }
264
+ return payload.data;
265
+ }
266
+
267
+ // src/client/SeamlessBoardApi.ts
268
+ var SeamlessBoardApi = class {
269
+ url;
270
+ apiKey;
271
+ /**
272
+ * @param config - GraphQL endpoint URL and access token.
273
+ */
274
+ constructor(config) {
275
+ if (!config?.url) {
276
+ throw new Error("SeamlessBoardApi: `url` is required.");
277
+ }
278
+ if (!config?.apiKey) {
279
+ throw new Error("SeamlessBoardApi: `apiKey` is required.");
280
+ }
281
+ this.url = config.url;
282
+ this.apiKey = config.apiKey;
283
+ }
284
+ /**
285
+ * Create a record for the given section.
286
+ *
287
+ * - Plain JSON fields → JSON POST.
288
+ * - Attachment fields (via {@link file}) → GraphQL multipart FormData POST.
289
+ *
290
+ * **Server-only:** throws {@link BrowserEnvironmentError} in the browser.
291
+ *
292
+ * @param input - Section ID and field data matching the board schema.
293
+ * @returns The created record `{ _id, no }`.
294
+ */
295
+ async addRecord(input) {
296
+ assertServerEnvironment("SeamlessBoardApi.addRecord");
297
+ if (!input?.section) {
298
+ throw new Error("addRecord: `section` is required.");
299
+ }
300
+ if (!input?.data || typeof input.data !== "object") {
301
+ throw new Error("addRecord: `data` is required.");
302
+ }
303
+ if (hasFileUploads(input.data)) {
304
+ return this.addRecordWithAttachments(input);
305
+ }
306
+ return this.addRecordJson(input);
307
+ }
308
+ /** JSON POST path (no attachments). */
309
+ async addRecordJson(input) {
310
+ const data = await graphqlRequest({
311
+ url: this.url,
312
+ apiKey: this.apiKey,
313
+ operationName: ADD_RECORD_OPERATION_NAME,
314
+ body: JSON.stringify({
315
+ query: ADD_RECORD_MUTATION,
316
+ variables: {
317
+ createRecordViaApiInput: {
318
+ section: input.section,
319
+ data: input.data
320
+ }
321
+ }
322
+ })
323
+ });
324
+ return data.addSeamlessBoardRecord;
325
+ }
326
+ /** Multipart POST path (one or more attachment fields). */
327
+ async addRecordWithAttachments(input) {
328
+ const { formData } = buildMultipartFormData(input.section, input.data);
329
+ const data = await graphqlRequest({
330
+ url: this.url,
331
+ apiKey: this.apiKey,
332
+ operationName: ADD_RECORD_OPERATION_NAME,
333
+ body: formData
334
+ });
335
+ return data.addSeamlessBoardRecord;
336
+ }
337
+ };
338
+ // Annotate the CommonJS export names for ESM import in node:
339
+ 0 && (module.exports = {
340
+ ApiError,
341
+ AutosocializerError,
342
+ BrowserEnvironmentError,
343
+ FileUpload,
344
+ SeamlessBoardApi,
345
+ file
346
+ });
@@ -0,0 +1,159 @@
1
+ /** Binary content accepted by {@link file}. */
2
+ type FileContent = Buffer | Uint8Array | ArrayBuffer | Blob;
3
+ /**
4
+ * Opaque wrapper marking a value as a file attachment for multipart upload.
5
+ * Create instances via {@link file} — do not construct this class directly.
6
+ */
7
+ declare class FileUpload {
8
+ readonly content: FileContent;
9
+ readonly filename: string;
10
+ readonly contentType: string;
11
+ /** @internal */
12
+ constructor(content: FileContent, filename: string, contentType: string);
13
+ }
14
+ /**
15
+ * Mark a binary value as a board attachment field.
16
+ * Content-type is optional — when omitted it is inferred from the filename.
17
+ *
18
+ * @param content - File bytes (`Buffer`, `Uint8Array`, `ArrayBuffer`, or `Blob`).
19
+ * @param filename - Original filename (used for MIME detection and upload name).
20
+ * @param contentType - Optional MIME override (e.g. `"application/pdf"`).
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { readFileSync } from "node:fs";
25
+ * import { file } from "@xtellig/autosocializer";
26
+ *
27
+ * const resume = file(readFileSync("./cv.pdf"), "cv.pdf");
28
+ * // or with an explicit type:
29
+ * const resume = file(buffer, "cv.pdf", "application/pdf");
30
+ * ```
31
+ */
32
+ declare function file(content: FileContent, filename: string, contentType?: string): FileUpload;
33
+
34
+ /**
35
+ * Configuration for {@link SeamlessBoardApi}.
36
+ */
37
+ interface SeamlessBoardApiConfig {
38
+ /**
39
+ * GraphQL endpoint URL.
40
+ * @example "https://xtellig.autosocializer.com/api/graphql-endpoint"
41
+ */
42
+ url: string;
43
+ /**
44
+ * Access token sent as `Authorization: Bearer <apiKey>`.
45
+ */
46
+ apiKey: string;
47
+ }
48
+ /**
49
+ * Scalar values accepted for non-attachment board fields.
50
+ */
51
+ type ScalarValue = string | number | boolean | null;
52
+ /**
53
+ * A single field value: a scalar, one file upload, or multiple file uploads.
54
+ */
55
+ type RecordValue = ScalarValue | FileUpload | FileUpload[];
56
+ /**
57
+ * Board field data keyed by field ID (e.g. `marks_number_4LbncDrii`).
58
+ */
59
+ type RecordData = Record<string, RecordValue>;
60
+ /**
61
+ * Input for {@link SeamlessBoardApi.addRecord}.
62
+ */
63
+ interface AddRecordInput {
64
+ /**
65
+ * Target board section ID.
66
+ * @example "67a448a8c78d6c21598d67d5"
67
+ */
68
+ section: string;
69
+ /**
70
+ * Field values keyed by field ID.
71
+ * Use {@link file} for attachment fields.
72
+ */
73
+ data: RecordData;
74
+ }
75
+ /**
76
+ * Successful response from `addSeamlessBoardRecord`.
77
+ */
78
+ interface AddRecordResult {
79
+ /** Created record ID. */
80
+ _id: string;
81
+ /** Created record number. */
82
+ no: number;
83
+ }
84
+
85
+ /**
86
+ * Client for Autosocializer SeamlessBoard GraphQL APIs.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * import { SeamlessBoardApi } from "@xtellig/autosocializer";
91
+ *
92
+ * const api = new SeamlessBoardApi({
93
+ * url: "https://xtellig.autosocializer.com/api/graphql-endpoint",
94
+ * apiKey: "TOKEN",
95
+ * });
96
+ *
97
+ * const res = await api.addRecord({
98
+ * section: "67a448a8c78d6c21598d67d5",
99
+ * data: {
100
+ * marks_number_4LbncDrii: 83,
101
+ * email_email_GOv6dM7VF: "user@example.com",
102
+ * },
103
+ * });
104
+ * ```
105
+ */
106
+ declare class SeamlessBoardApi {
107
+ private readonly url;
108
+ private readonly apiKey;
109
+ /**
110
+ * @param config - GraphQL endpoint URL and access token.
111
+ */
112
+ constructor(config: SeamlessBoardApiConfig);
113
+ /**
114
+ * Create a record for the given section.
115
+ *
116
+ * - Plain JSON fields → JSON POST.
117
+ * - Attachment fields (via {@link file}) → GraphQL multipart FormData POST.
118
+ *
119
+ * **Server-only:** throws {@link BrowserEnvironmentError} in the browser.
120
+ *
121
+ * @param input - Section ID and field data matching the board schema.
122
+ * @returns The created record `{ _id, no }`.
123
+ */
124
+ addRecord(input: AddRecordInput): Promise<AddRecordResult>;
125
+ /** JSON POST path (no attachments). */
126
+ private addRecordJson;
127
+ /** Multipart POST path (one or more attachment fields). */
128
+ private addRecordWithAttachments;
129
+ }
130
+
131
+ /**
132
+ * Base error for all @xtellig/autosocializer failures.
133
+ */
134
+ declare class AutosocializerError extends Error {
135
+ constructor(message: string);
136
+ }
137
+ /**
138
+ * Thrown when a GraphQL or HTTP request fails.
139
+ */
140
+ declare class ApiError extends AutosocializerError {
141
+ /** HTTP status code when available. */
142
+ readonly status?: number;
143
+ /** Raw GraphQL error payload when available. */
144
+ readonly errors?: unknown[];
145
+ constructor(message: string, options?: {
146
+ status?: number;
147
+ errors?: unknown[];
148
+ });
149
+ }
150
+ /**
151
+ * Thrown when a server-only feature is invoked in a browser environment.
152
+ */
153
+ declare class BrowserEnvironmentError extends AutosocializerError {
154
+ /** Feature name that was blocked (e.g. "SeamlessBoardApi.addRecord"). */
155
+ readonly feature: string;
156
+ constructor(feature: string);
157
+ }
158
+
159
+ export { type AddRecordInput, type AddRecordResult, ApiError, AutosocializerError, BrowserEnvironmentError, type FileContent, FileUpload, type RecordData, type RecordValue, type ScalarValue, SeamlessBoardApi, type SeamlessBoardApiConfig, file };
@@ -0,0 +1,159 @@
1
+ /** Binary content accepted by {@link file}. */
2
+ type FileContent = Buffer | Uint8Array | ArrayBuffer | Blob;
3
+ /**
4
+ * Opaque wrapper marking a value as a file attachment for multipart upload.
5
+ * Create instances via {@link file} — do not construct this class directly.
6
+ */
7
+ declare class FileUpload {
8
+ readonly content: FileContent;
9
+ readonly filename: string;
10
+ readonly contentType: string;
11
+ /** @internal */
12
+ constructor(content: FileContent, filename: string, contentType: string);
13
+ }
14
+ /**
15
+ * Mark a binary value as a board attachment field.
16
+ * Content-type is optional — when omitted it is inferred from the filename.
17
+ *
18
+ * @param content - File bytes (`Buffer`, `Uint8Array`, `ArrayBuffer`, or `Blob`).
19
+ * @param filename - Original filename (used for MIME detection and upload name).
20
+ * @param contentType - Optional MIME override (e.g. `"application/pdf"`).
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { readFileSync } from "node:fs";
25
+ * import { file } from "@xtellig/autosocializer";
26
+ *
27
+ * const resume = file(readFileSync("./cv.pdf"), "cv.pdf");
28
+ * // or with an explicit type:
29
+ * const resume = file(buffer, "cv.pdf", "application/pdf");
30
+ * ```
31
+ */
32
+ declare function file(content: FileContent, filename: string, contentType?: string): FileUpload;
33
+
34
+ /**
35
+ * Configuration for {@link SeamlessBoardApi}.
36
+ */
37
+ interface SeamlessBoardApiConfig {
38
+ /**
39
+ * GraphQL endpoint URL.
40
+ * @example "https://xtellig.autosocializer.com/api/graphql-endpoint"
41
+ */
42
+ url: string;
43
+ /**
44
+ * Access token sent as `Authorization: Bearer <apiKey>`.
45
+ */
46
+ apiKey: string;
47
+ }
48
+ /**
49
+ * Scalar values accepted for non-attachment board fields.
50
+ */
51
+ type ScalarValue = string | number | boolean | null;
52
+ /**
53
+ * A single field value: a scalar, one file upload, or multiple file uploads.
54
+ */
55
+ type RecordValue = ScalarValue | FileUpload | FileUpload[];
56
+ /**
57
+ * Board field data keyed by field ID (e.g. `marks_number_4LbncDrii`).
58
+ */
59
+ type RecordData = Record<string, RecordValue>;
60
+ /**
61
+ * Input for {@link SeamlessBoardApi.addRecord}.
62
+ */
63
+ interface AddRecordInput {
64
+ /**
65
+ * Target board section ID.
66
+ * @example "67a448a8c78d6c21598d67d5"
67
+ */
68
+ section: string;
69
+ /**
70
+ * Field values keyed by field ID.
71
+ * Use {@link file} for attachment fields.
72
+ */
73
+ data: RecordData;
74
+ }
75
+ /**
76
+ * Successful response from `addSeamlessBoardRecord`.
77
+ */
78
+ interface AddRecordResult {
79
+ /** Created record ID. */
80
+ _id: string;
81
+ /** Created record number. */
82
+ no: number;
83
+ }
84
+
85
+ /**
86
+ * Client for Autosocializer SeamlessBoard GraphQL APIs.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * import { SeamlessBoardApi } from "@xtellig/autosocializer";
91
+ *
92
+ * const api = new SeamlessBoardApi({
93
+ * url: "https://xtellig.autosocializer.com/api/graphql-endpoint",
94
+ * apiKey: "TOKEN",
95
+ * });
96
+ *
97
+ * const res = await api.addRecord({
98
+ * section: "67a448a8c78d6c21598d67d5",
99
+ * data: {
100
+ * marks_number_4LbncDrii: 83,
101
+ * email_email_GOv6dM7VF: "user@example.com",
102
+ * },
103
+ * });
104
+ * ```
105
+ */
106
+ declare class SeamlessBoardApi {
107
+ private readonly url;
108
+ private readonly apiKey;
109
+ /**
110
+ * @param config - GraphQL endpoint URL and access token.
111
+ */
112
+ constructor(config: SeamlessBoardApiConfig);
113
+ /**
114
+ * Create a record for the given section.
115
+ *
116
+ * - Plain JSON fields → JSON POST.
117
+ * - Attachment fields (via {@link file}) → GraphQL multipart FormData POST.
118
+ *
119
+ * **Server-only:** throws {@link BrowserEnvironmentError} in the browser.
120
+ *
121
+ * @param input - Section ID and field data matching the board schema.
122
+ * @returns The created record `{ _id, no }`.
123
+ */
124
+ addRecord(input: AddRecordInput): Promise<AddRecordResult>;
125
+ /** JSON POST path (no attachments). */
126
+ private addRecordJson;
127
+ /** Multipart POST path (one or more attachment fields). */
128
+ private addRecordWithAttachments;
129
+ }
130
+
131
+ /**
132
+ * Base error for all @xtellig/autosocializer failures.
133
+ */
134
+ declare class AutosocializerError extends Error {
135
+ constructor(message: string);
136
+ }
137
+ /**
138
+ * Thrown when a GraphQL or HTTP request fails.
139
+ */
140
+ declare class ApiError extends AutosocializerError {
141
+ /** HTTP status code when available. */
142
+ readonly status?: number;
143
+ /** Raw GraphQL error payload when available. */
144
+ readonly errors?: unknown[];
145
+ constructor(message: string, options?: {
146
+ status?: number;
147
+ errors?: unknown[];
148
+ });
149
+ }
150
+ /**
151
+ * Thrown when a server-only feature is invoked in a browser environment.
152
+ */
153
+ declare class BrowserEnvironmentError extends AutosocializerError {
154
+ /** Feature name that was blocked (e.g. "SeamlessBoardApi.addRecord"). */
155
+ readonly feature: string;
156
+ constructor(feature: string);
157
+ }
158
+
159
+ export { type AddRecordInput, type AddRecordResult, ApiError, AutosocializerError, BrowserEnvironmentError, type FileContent, FileUpload, type RecordData, type RecordValue, type ScalarValue, SeamlessBoardApi, type SeamlessBoardApiConfig, file };
package/dist/index.js ADDED
@@ -0,0 +1,314 @@
1
+ // src/errors.ts
2
+ var AutosocializerError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "AutosocializerError";
6
+ }
7
+ };
8
+ var ApiError = class extends AutosocializerError {
9
+ /** HTTP status code when available. */
10
+ status;
11
+ /** Raw GraphQL error payload when available. */
12
+ errors;
13
+ constructor(message, options) {
14
+ super(message);
15
+ this.name = "ApiError";
16
+ this.status = options?.status;
17
+ this.errors = options?.errors;
18
+ }
19
+ };
20
+ var BrowserEnvironmentError = class extends AutosocializerError {
21
+ /** Feature name that was blocked (e.g. "SeamlessBoardApi.addRecord"). */
22
+ feature;
23
+ constructor(feature) {
24
+ super(
25
+ `${feature} is only available in a server environment (Node.js). It cannot run in the browser. Call it from your backend instead.`
26
+ );
27
+ this.name = "BrowserEnvironmentError";
28
+ this.feature = feature;
29
+ }
30
+ };
31
+
32
+ // src/env/guard.ts
33
+ function isBrowserEnvironment() {
34
+ return typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
35
+ }
36
+ function assertServerEnvironment(feature) {
37
+ if (isBrowserEnvironment()) {
38
+ throw new BrowserEnvironmentError(feature);
39
+ }
40
+ }
41
+
42
+ // src/graphql/mutation.ts
43
+ var ADD_RECORD_MUTATION = `
44
+ mutation addSeamlessBoardRecord($createRecordViaApiInput: CreateRecordViaApiInput!) {
45
+ addSeamlessBoardRecord(createRecordViaApiInput: $createRecordViaApiInput) {
46
+ _id
47
+ no
48
+ }
49
+ }
50
+ `.trim();
51
+ var ADD_RECORD_OPERATION_NAME = "addSeamlessBoardRecord";
52
+
53
+ // src/attachments/mime.ts
54
+ var MIME_BY_EXT = {
55
+ // Documents
56
+ pdf: "application/pdf",
57
+ doc: "application/msword",
58
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
59
+ xls: "application/vnd.ms-excel",
60
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
61
+ ppt: "application/vnd.ms-powerpoint",
62
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
63
+ txt: "text/plain",
64
+ csv: "text/csv",
65
+ json: "application/json",
66
+ xml: "application/xml",
67
+ // Images
68
+ png: "image/png",
69
+ jpg: "image/jpeg",
70
+ jpeg: "image/jpeg",
71
+ gif: "image/gif",
72
+ webp: "image/webp",
73
+ svg: "image/svg+xml",
74
+ ico: "image/x-icon",
75
+ bmp: "image/bmp",
76
+ // Audio / video
77
+ mp3: "audio/mpeg",
78
+ wav: "audio/wav",
79
+ ogg: "audio/ogg",
80
+ mp4: "video/mp4",
81
+ webm: "video/webm",
82
+ // Archives
83
+ zip: "application/zip",
84
+ rar: "application/vnd.rar",
85
+ "7z": "application/x-7z-compressed",
86
+ gz: "application/gzip",
87
+ tar: "application/x-tar"
88
+ };
89
+ function resolveMime(filename, contentType) {
90
+ if (contentType && contentType.trim()) {
91
+ return contentType.trim();
92
+ }
93
+ const ext = filename.split(".").pop()?.toLowerCase() ?? "";
94
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
95
+ }
96
+
97
+ // src/attachments/file.ts
98
+ var FileUpload = class {
99
+ content;
100
+ filename;
101
+ contentType;
102
+ /** @internal */
103
+ constructor(content, filename, contentType) {
104
+ this.content = content;
105
+ this.filename = filename;
106
+ this.contentType = contentType;
107
+ }
108
+ };
109
+ function file(content, filename, contentType) {
110
+ return new FileUpload(content, filename, resolveMime(filename, contentType));
111
+ }
112
+ function isFileUpload(value) {
113
+ return value instanceof FileUpload;
114
+ }
115
+
116
+ // src/graphql/multipart.ts
117
+ function dummyKey() {
118
+ return `dummy-${Math.random().toString(36).substring(2, 10)}`;
119
+ }
120
+ function toBlob(upload) {
121
+ if (upload.content instanceof Blob) {
122
+ return upload.content.type ? upload.content : new Blob([upload.content], { type: upload.contentType });
123
+ }
124
+ return new Blob([upload.content], {
125
+ type: upload.contentType
126
+ });
127
+ }
128
+ function transformField(value, fieldId, parts) {
129
+ if (isFileUpload(value)) {
130
+ const key = dummyKey();
131
+ parts.push({
132
+ key: String(parts.length + 1),
133
+ path: `variables.createRecordViaApiInput.data.${fieldId}.${key}`,
134
+ upload: value
135
+ });
136
+ return { [key]: null };
137
+ }
138
+ if (Array.isArray(value) && value.some(isFileUpload)) {
139
+ const obj = {};
140
+ for (const item of value) {
141
+ if (!isFileUpload(item)) continue;
142
+ const key = dummyKey();
143
+ parts.push({
144
+ key: String(parts.length + 1),
145
+ path: `variables.createRecordViaApiInput.data.${fieldId}.${key}`,
146
+ upload: item
147
+ });
148
+ obj[key] = null;
149
+ }
150
+ return obj;
151
+ }
152
+ return value;
153
+ }
154
+ function hasFileUploads(data) {
155
+ for (const value of Object.values(data)) {
156
+ if (isFileUpload(value)) return true;
157
+ if (Array.isArray(value) && value.some(isFileUpload)) return true;
158
+ }
159
+ return false;
160
+ }
161
+ function buildMultipartFormData(section, data) {
162
+ const parts = [];
163
+ const transformed = {};
164
+ for (const [fieldId, value] of Object.entries(data)) {
165
+ transformed[fieldId] = transformField(value, fieldId, parts);
166
+ }
167
+ const operations = {
168
+ query: ADD_RECORD_MUTATION,
169
+ variables: {
170
+ createRecordViaApiInput: {
171
+ section,
172
+ data: transformed
173
+ }
174
+ }
175
+ };
176
+ const map = {};
177
+ for (const part of parts) {
178
+ map[part.key] = [part.path];
179
+ }
180
+ const formData = new FormData();
181
+ formData.append("operations", JSON.stringify(operations));
182
+ formData.append("map", JSON.stringify(map));
183
+ for (const part of parts) {
184
+ formData.append(part.key, toBlob(part.upload), part.upload.filename);
185
+ }
186
+ return { formData, hasAttachments: parts.length > 0 };
187
+ }
188
+
189
+ // src/http/request.ts
190
+ async function graphqlRequest(options) {
191
+ const headers = {
192
+ Authorization: `Bearer ${options.apiKey}`,
193
+ "x-apollo-operation-name": options.operationName,
194
+ ...options.headers
195
+ };
196
+ if (typeof options.body === "string") {
197
+ headers["Content-Type"] = "application/json";
198
+ }
199
+ const response = await fetch(options.url, {
200
+ method: "POST",
201
+ headers,
202
+ body: options.body
203
+ });
204
+ let payload;
205
+ const text = await response.text();
206
+ try {
207
+ payload = text ? JSON.parse(text) : void 0;
208
+ } catch {
209
+ throw new ApiError(
210
+ `Invalid JSON response from GraphQL endpoint (HTTP ${response.status}).`,
211
+ { status: response.status }
212
+ );
213
+ }
214
+ if (!response.ok) {
215
+ const message = payload?.errors?.map((e) => e.message).filter(Boolean).join("; ") || `GraphQL request failed with HTTP ${response.status}.`;
216
+ throw new ApiError(message, {
217
+ status: response.status,
218
+ errors: payload?.errors
219
+ });
220
+ }
221
+ if (payload?.errors?.length) {
222
+ const message = payload.errors.map((e) => e.message).filter(Boolean).join("; ") || "GraphQL request returned errors.";
223
+ throw new ApiError(message, {
224
+ status: response.status,
225
+ errors: payload.errors
226
+ });
227
+ }
228
+ if (!payload?.data) {
229
+ throw new ApiError("GraphQL response contained no data.", {
230
+ status: response.status
231
+ });
232
+ }
233
+ return payload.data;
234
+ }
235
+
236
+ // src/client/SeamlessBoardApi.ts
237
+ var SeamlessBoardApi = class {
238
+ url;
239
+ apiKey;
240
+ /**
241
+ * @param config - GraphQL endpoint URL and access token.
242
+ */
243
+ constructor(config) {
244
+ if (!config?.url) {
245
+ throw new Error("SeamlessBoardApi: `url` is required.");
246
+ }
247
+ if (!config?.apiKey) {
248
+ throw new Error("SeamlessBoardApi: `apiKey` is required.");
249
+ }
250
+ this.url = config.url;
251
+ this.apiKey = config.apiKey;
252
+ }
253
+ /**
254
+ * Create a record for the given section.
255
+ *
256
+ * - Plain JSON fields → JSON POST.
257
+ * - Attachment fields (via {@link file}) → GraphQL multipart FormData POST.
258
+ *
259
+ * **Server-only:** throws {@link BrowserEnvironmentError} in the browser.
260
+ *
261
+ * @param input - Section ID and field data matching the board schema.
262
+ * @returns The created record `{ _id, no }`.
263
+ */
264
+ async addRecord(input) {
265
+ assertServerEnvironment("SeamlessBoardApi.addRecord");
266
+ if (!input?.section) {
267
+ throw new Error("addRecord: `section` is required.");
268
+ }
269
+ if (!input?.data || typeof input.data !== "object") {
270
+ throw new Error("addRecord: `data` is required.");
271
+ }
272
+ if (hasFileUploads(input.data)) {
273
+ return this.addRecordWithAttachments(input);
274
+ }
275
+ return this.addRecordJson(input);
276
+ }
277
+ /** JSON POST path (no attachments). */
278
+ async addRecordJson(input) {
279
+ const data = await graphqlRequest({
280
+ url: this.url,
281
+ apiKey: this.apiKey,
282
+ operationName: ADD_RECORD_OPERATION_NAME,
283
+ body: JSON.stringify({
284
+ query: ADD_RECORD_MUTATION,
285
+ variables: {
286
+ createRecordViaApiInput: {
287
+ section: input.section,
288
+ data: input.data
289
+ }
290
+ }
291
+ })
292
+ });
293
+ return data.addSeamlessBoardRecord;
294
+ }
295
+ /** Multipart POST path (one or more attachment fields). */
296
+ async addRecordWithAttachments(input) {
297
+ const { formData } = buildMultipartFormData(input.section, input.data);
298
+ const data = await graphqlRequest({
299
+ url: this.url,
300
+ apiKey: this.apiKey,
301
+ operationName: ADD_RECORD_OPERATION_NAME,
302
+ body: formData
303
+ });
304
+ return data.addSeamlessBoardRecord;
305
+ }
306
+ };
307
+ export {
308
+ ApiError,
309
+ AutosocializerError,
310
+ BrowserEnvironmentError,
311
+ FileUpload,
312
+ SeamlessBoardApi,
313
+ file
314
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@xtellig/autosocializer",
3
+ "version": "1.0.0",
4
+ "description": "The official Node.js SDK for streaming backend server data directly to Autosocializer boards, triggering automated workflows with zero manual intervention",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "LICENSE",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "keywords": [
26
+ "xtellig",
27
+ "autosocializer",
28
+ "seamless-board",
29
+ "graphql",
30
+ "sdk",
31
+ "api-client"
32
+ ],
33
+ "author": "Xtellig",
34
+ "license": "MIT",
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/xtellig/autosocializer.git"
44
+ },
45
+ "homepage": "https://www.xtellig.com",
46
+ "bugs": {
47
+ "url": "https://github.com/xtellig/autosocializer/issues"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^22.13.10",
51
+ "tsup": "^8.4.0",
52
+ "typescript": "^5.8.2"
53
+ }
54
+ }