@ophelio/sdk 0.1.1 → 0.2.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/dist/index.mjs CHANGED
@@ -116,6 +116,15 @@ function sleep(ms) {
116
116
  const DEFAULT_BASE_URL = "https://api.ophel.io";
117
117
  const DEFAULT_TIMEOUT_MS = 3e4;
118
118
  const DEFAULT_MAX_RETRIES = 2;
119
+ /**
120
+ * Resolves the caller-supplied idempotency key to the value sent on the wire.
121
+ * Trims it and falls back to a generated key when it is absent, empty, or
122
+ * whitespace-only — a blank key must never leave an idempotency-keyed mutation
123
+ * retryable but unkeyed, or a retried write could duplicate.
124
+ */
125
+ function resolveIdempotencyKey(idempotencyKey) {
126
+ return idempotencyKey?.trim() || crypto.randomUUID();
127
+ }
119
128
  var Transport = class {
120
129
  apiKey;
121
130
  baseUrl;
@@ -215,6 +224,13 @@ var APIResource = class {
215
224
  this.transport = transport;
216
225
  }
217
226
  };
227
+ function pageQuery(params) {
228
+ if (!params) return void 0;
229
+ const query = {};
230
+ if (params.page_size !== void 0) query.page_size = String(params.page_size);
231
+ if (params.page_token !== void 0) query.page_token = params.page_token;
232
+ return query;
233
+ }
218
234
  //#endregion
219
235
  //#region src/resources/admit.ts
220
236
  var AdmitResource = class extends APIResource {
@@ -299,13 +315,19 @@ var BillingOptionsResource = class extends APIResource {
299
315
  //#endregion
300
316
  //#region src/resources/customers.ts
301
317
  var CustomersResource = class extends APIResource {
302
- async list(options) {
303
- return (await this.transport.request({
318
+ async list(params, options) {
319
+ const response = await this.transport.request({
304
320
  method: "GET",
305
321
  path: "/api/v1/customers",
322
+ query: pageQuery(params),
306
323
  retryable: true,
307
324
  options
308
- })).customers;
325
+ });
326
+ return {
327
+ items: response.customers,
328
+ next_page_token: response.next_page_token,
329
+ total_size: response.total_size
330
+ };
309
331
  }
310
332
  get(id, options) {
311
333
  return this.transport.request({
@@ -341,13 +363,19 @@ var CustomersResource = class extends APIResource {
341
363
  options
342
364
  });
343
365
  }
344
- async listMemberships(id, options) {
345
- return (await this.transport.request({
366
+ async listMemberships(id, params, options) {
367
+ const response = await this.transport.request({
346
368
  method: "GET",
347
369
  path: `/api/v1/customers/${encodeURIComponent(id)}/memberships`,
370
+ query: pageQuery(params),
348
371
  retryable: true,
349
372
  options
350
- })).memberships;
373
+ });
374
+ return {
375
+ items: response.memberships,
376
+ next_page_token: response.next_page_token,
377
+ total_size: response.total_size
378
+ };
351
379
  }
352
380
  };
353
381
  //#endregion
@@ -364,7 +392,7 @@ var EntitlementsResource = class extends APIResource {
364
392
  method: "POST",
365
393
  path: "/api/v1/entitlements/redeem",
366
394
  body: params,
367
- idempotencyKey: idempotencyKey ?? crypto.randomUUID(),
395
+ idempotencyKey: resolveIdempotencyKey(idempotencyKey),
368
396
  retryable: true,
369
397
  options: requestOptions
370
398
  });
@@ -379,7 +407,7 @@ var EntitlementsResource = class extends APIResource {
379
407
  method: "POST",
380
408
  path: "/api/v1/entitlements/usage",
381
409
  body: params,
382
- idempotencyKey: idempotencyKey ?? crypto.randomUUID(),
410
+ idempotencyKey: resolveIdempotencyKey(idempotencyKey),
383
411
  retryable: true,
384
412
  options: requestOptions
385
413
  })).usage;
@@ -520,13 +548,19 @@ var MembersResource = class extends APIResource {
520
548
  //#endregion
521
549
  //#region src/resources/memberships.ts
522
550
  var MembershipsResource = class extends APIResource {
523
- async list(options) {
524
- return (await this.transport.request({
551
+ async list(params, options) {
552
+ const response = await this.transport.request({
525
553
  method: "GET",
526
554
  path: "/api/v1/memberships",
555
+ query: pageQuery(params),
527
556
  retryable: true,
528
557
  options
529
- })).memberships;
558
+ });
559
+ return {
560
+ items: response.memberships,
561
+ next_page_token: response.next_page_token,
562
+ total_size: response.total_size
563
+ };
530
564
  }
531
565
  get(id, options) {
532
566
  return this.transport.request({
@@ -536,13 +570,22 @@ var MembershipsResource = class extends APIResource {
536
570
  options
537
571
  });
538
572
  }
539
- create(params, options) {
573
+ /**
574
+ * Create a membership atomically (record, members, first term, optional
575
+ * payment). Idempotent: the Idempotency-Key is generated once per call (and
576
+ * reused across the SDK's internal retries), so a retried create returns the
577
+ * original membership instead of creating a duplicate. Supply your own key to
578
+ * dedupe application-level retries across processes.
579
+ */
580
+ create(params, options = {}) {
581
+ const { idempotencyKey, ...requestOptions } = options;
540
582
  return this.transport.request({
541
583
  method: "POST",
542
584
  path: "/api/v1/memberships",
543
585
  body: params,
544
- retryable: false,
545
- options
586
+ idempotencyKey: resolveIdempotencyKey(idempotencyKey),
587
+ retryable: true,
588
+ options: requestOptions
546
589
  });
547
590
  }
548
591
  update(id, params, options) {
@@ -572,25 +615,36 @@ var MembershipsResource = class extends APIResource {
572
615
  options
573
616
  });
574
617
  }
575
- async listTransactions(id, options) {
576
- return (await this.transport.request({
618
+ async listTransactions(id, params, options) {
619
+ const response = await this.transport.request({
577
620
  method: "GET",
578
621
  path: `/api/v1/memberships/${encodeURIComponent(id)}/transactions`,
622
+ query: pageQuery(params),
579
623
  retryable: true,
580
624
  options
581
- })).transactions;
625
+ });
626
+ return {
627
+ items: response.transactions,
628
+ next_page_token: response.next_page_token,
629
+ total_size: response.total_size
630
+ };
582
631
  }
583
632
  /**
584
633
  * Report a payment outcome. Ophel.io never processes payments — your
585
634
  * payment provider does; this records the result and drives dunning.
635
+ * Idempotent: the Idempotency-Key is generated once per call (and reused
636
+ * across the SDK's internal retries), so a retried report records the
637
+ * transaction once and does not advance dunning twice.
586
638
  */
587
- createTransaction(id, params, options) {
639
+ createTransaction(id, params, options = {}) {
640
+ const { idempotencyKey, ...requestOptions } = options;
588
641
  return this.transport.request({
589
642
  method: "POST",
590
643
  path: `/api/v1/memberships/${encodeURIComponent(id)}/transactions`,
591
644
  body: params,
592
- retryable: false,
593
- options
645
+ idempotencyKey: resolveIdempotencyKey(idempotencyKey),
646
+ retryable: true,
647
+ options: requestOptions
594
648
  });
595
649
  }
596
650
  async listMembers(id, options) {
@@ -601,14 +655,21 @@ var MembershipsResource = class extends APIResource {
601
655
  options
602
656
  })).members;
603
657
  }
604
- /** Open the next term. Rejected (409) while cancelled or cancellation-pending. */
605
- renew(id, params, options) {
658
+ /**
659
+ * Open the next term. Rejected (409) while cancelled or cancellation-pending.
660
+ * Idempotent: the Idempotency-Key is generated once per call (and reused
661
+ * across the SDK's internal retries), so a retried renewal returns the
662
+ * original result instead of opening a second term.
663
+ */
664
+ renew(id, params, options = {}) {
665
+ const { idempotencyKey, ...requestOptions } = options;
606
666
  return this.transport.request({
607
667
  method: "POST",
608
668
  path: `/api/v1/memberships/${encodeURIComponent(id)}/renew`,
609
669
  body: params,
610
- retryable: false,
611
- options
670
+ idempotencyKey: resolveIdempotencyKey(idempotencyKey),
671
+ retryable: true,
672
+ options: requestOptions
612
673
  });
613
674
  }
614
675
  /**
@@ -635,13 +696,21 @@ var MembershipsResource = class extends APIResource {
635
696
  });
636
697
  }
637
698
  async listScheduledChanges(id, params, options) {
638
- return (await this.transport.request({
699
+ const response = await this.transport.request({
639
700
  method: "GET",
640
701
  path: `/api/v1/memberships/${encodeURIComponent(id)}/scheduled-changes`,
641
- query: params?.status ? { status: params.status } : void 0,
702
+ query: {
703
+ ...pageQuery(params),
704
+ ...params?.status ? { status: params.status } : {}
705
+ },
642
706
  retryable: true,
643
707
  options
644
- })).scheduled_changes;
708
+ });
709
+ return {
710
+ items: response.scheduled_changes,
711
+ next_page_token: response.next_page_token,
712
+ total_size: response.total_size
713
+ };
645
714
  }
646
715
  async cancelScheduledChange(id, changeId, options) {
647
716
  await this.transport.request({
@@ -651,13 +720,19 @@ var MembershipsResource = class extends APIResource {
651
720
  options
652
721
  });
653
722
  }
654
- async listTermTransactions(id, termId, options) {
655
- return (await this.transport.request({
723
+ async listTermTransactions(id, termId, params, options) {
724
+ const response = await this.transport.request({
656
725
  method: "GET",
657
726
  path: `/api/v1/memberships/${encodeURIComponent(id)}/terms/${encodeURIComponent(termId)}/transactions`,
727
+ query: pageQuery(params),
658
728
  retryable: true,
659
729
  options
660
- })).transactions;
730
+ });
731
+ return {
732
+ items: response.transactions,
733
+ next_page_token: response.next_page_token,
734
+ total_size: response.total_size
735
+ };
661
736
  }
662
737
  };
663
738
  //#endregion
@@ -941,13 +1016,19 @@ var WebhookSubscriptionsResource = class extends APIResource {
941
1016
  options
942
1017
  });
943
1018
  }
944
- async listDeliveries(id, options) {
945
- return (await this.transport.request({
1019
+ async listDeliveries(id, params, options) {
1020
+ const response = await this.transport.request({
946
1021
  method: "GET",
947
1022
  path: `/api/v1/webhook-subscriptions/${encodeURIComponent(id)}/deliveries`,
1023
+ query: pageQuery(params),
948
1024
  retryable: true,
949
1025
  options
950
- })).webhook_deliveries;
1026
+ });
1027
+ return {
1028
+ items: response.webhook_deliveries,
1029
+ next_page_token: response.next_page_token,
1030
+ total_size: response.total_size
1031
+ };
951
1032
  }
952
1033
  /** The HMAC signing secret is only returned on creation — store it then. */
953
1034
  create(params, options) {
@@ -1071,6 +1152,52 @@ var Ophelio = class Ophelio {
1071
1152
  this.webhookSubscriptions = new WebhookSubscriptionsResource(transport);
1072
1153
  }
1073
1154
  };
1155
+ //#endregion
1156
+ //#region src/core/pagination.ts
1157
+ /**
1158
+ * Streams every item across all pages of a paginated `list*` method, fetching
1159
+ * the next page lazily as you iterate:
1160
+ *
1161
+ * ```ts
1162
+ * for await (const membership of paginate((page) => ophelio.memberships.list(page))) {
1163
+ * // …
1164
+ * }
1165
+ *
1166
+ * // nested + filtered + abortable:
1167
+ * for await (const change of paginate((page) =>
1168
+ * ophelio.memberships.listScheduledChanges(id, { ...page, status: 'all' }, { signal }))) {
1169
+ * // …
1170
+ * }
1171
+ * ```
1172
+ *
1173
+ * Because this walks offset pages, a row may be seen twice or skipped across a
1174
+ * page boundary if the collection is written to concurrently.
1175
+ */
1176
+ async function* paginate(fetchPage, params = {}) {
1177
+ let pageToken = params.page_token;
1178
+ do {
1179
+ const page = await fetchPage({
1180
+ ...params,
1181
+ page_token: pageToken
1182
+ });
1183
+ yield* page.items;
1184
+ pageToken = page.next_page_token || void 0;
1185
+ } while (pageToken);
1186
+ }
1187
+ /**
1188
+ * Drains every page into a single array — the "give me everything" shortcut
1189
+ * over `paginate`. Loads the whole collection into memory, so prefer
1190
+ * `paginate` for large or unbounded lists.
1191
+ *
1192
+ * ```ts
1193
+ * const customers = await collect((page) => ophelio.customers.list(page))
1194
+ * ```
1195
+ */
1196
+ async function collect(fetchPage, params = {}) {
1197
+ const items = [];
1198
+ for await (const item of paginate(fetchPage, params)) items.push(item);
1199
+ return items;
1200
+ }
1074
1201
  /**
1075
1202
  * Canonical webhook event codes for the Ophel.io platform.
1076
1203
  *
@@ -1095,4 +1222,4 @@ const WEBHOOK_EVENT_CODES = [
1095
1222
  "plan.updated"
1096
1223
  ];
1097
1224
  //#endregion
1098
- export { AlreadyExistsError, DEFAULT_BASE_URL, InternalError, InvalidArgumentError, NotFoundError, Ophelio, OphelioConnectionError, OphelioError, OphelioTimeoutError, PermissionDeniedError, ResourceExhaustedError, UnauthenticatedError, UnavailableError, UnimplementedError, WEBHOOK_EVENT_CODES, WebhookSignatureVerificationError, constructEvent, verifyWebhookSignature };
1225
+ export { AlreadyExistsError, DEFAULT_BASE_URL, InternalError, InvalidArgumentError, NotFoundError, Ophelio, OphelioConnectionError, OphelioError, OphelioTimeoutError, PermissionDeniedError, ResourceExhaustedError, UnauthenticatedError, UnavailableError, UnimplementedError, WEBHOOK_EVENT_CODES, WebhookSignatureVerificationError, collect, constructEvent, paginate, verifyWebhookSignature };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ophelio/sdk",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Official JavaScript / TypeScript SDK for the Ophel.io membership & entitlement API.",
5
5
  "license": "MIT",
6
6
  "author": "Ophel.io",
@@ -37,11 +37,14 @@
37
37
  "pretest": "npm run generate:types",
38
38
  "test": "vitest run",
39
39
  "pretypecheck": "npm run generate:types",
40
- "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json"
40
+ "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
41
+ "check:package": "npm run build && publint --strict && attw --pack"
41
42
  },
42
43
  "devDependencies": {
44
+ "@arethetypeswrong/cli": "^0.18.0",
43
45
  "@types/node": "^25.6.0",
44
46
  "ophelio-utils": "*",
47
+ "publint": "^0.3.21",
45
48
  "tsdown": "^0.22.0",
46
49
  "typescript": "^6.0.3",
47
50
  "vitest": "^4.0.18"