@loopwise/sdk 0.1.0-beta.1 → 0.1.0-beta.2

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/config.mjs CHANGED
@@ -2,6 +2,5 @@
2
2
  function defineConfig(config) {
3
3
  return config;
4
4
  }
5
-
6
5
  //#endregion
7
- export { defineConfig };
6
+ export { defineConfig };
package/dist/index.d.mts CHANGED
@@ -119,6 +119,34 @@ interface Lecturer {
119
119
  name: string;
120
120
  avatar: string | null;
121
121
  }
122
+ interface Post {
123
+ id: string;
124
+ slug: string;
125
+ title: string;
126
+ subtitle: string | null;
127
+ excerpt: string | null;
128
+ /** Full body HTML. Truncated server-side when access is gated and the
129
+ * viewer doesn't have permission (login_required without auth, or paid
130
+ * without an active subscription). */
131
+ body: string;
132
+ /** Maps to schema `Post.cover_photo`. */
133
+ coverImage: string | null;
134
+ /** Unix timestamp (seconds since epoch). */
135
+ publishedAt: number;
136
+ /** Unix timestamp of the last update — schema sources this from the
137
+ * post's `updated_at`, so for any persisted record this is populated
138
+ * (typically equal to or after `publishedAt`). The `null` is included
139
+ * for forward-compat with a hypothetical "never modified" sentinel
140
+ * but isn't observed in current production data. */
141
+ modifiedAt: number | null;
142
+ /** Known values: `login_required`, `paid`, `public_access`. The
143
+ * `string & {}` escape hatch keeps autocomplete on the known set
144
+ * while accepting forward-compat values the schema may add. */
145
+ accessType: 'login_required' | 'paid' | 'public_access' | (string & {});
146
+ tags: string[] | null;
147
+ noindex: boolean;
148
+ author: Lecturer | null;
149
+ }
122
150
  interface PageInfo {
123
151
  page: number;
124
152
  totalPages: number;
@@ -130,18 +158,42 @@ interface ListResult<T> {
130
158
  items: T[];
131
159
  pageInfo: PageInfo;
132
160
  }
161
+ /** Simplified order status. Mirrors the storefront `Order.status` enum. */
162
+ type OrderStatus = 'PAID' | 'PENDING' | 'FAILED' | 'REFUNDED';
163
+ interface OrderItem {
164
+ /**
165
+ * Meta-pixel-aligned identifier (Lineitem#tracking_id server-side).
166
+ * Use directly in `content_ids` / `contents[].id` for Purchase events.
167
+ */
168
+ id: string;
169
+ name: string | null;
170
+ quantity: number;
171
+ /** Per-unit price in the order currency. Maps to FB Pixel `contents[].item_price`. */
172
+ amount: number;
173
+ }
174
+ interface Order {
175
+ /** Stable order identifier — currently equals `Payment#trade_no` server-side. */
176
+ id: string;
177
+ status: OrderStatus;
178
+ /** Total paid amount in the order currency. Maps to FB Pixel `value`. */
179
+ total: number;
180
+ currency: string;
181
+ items: OrderItem[];
182
+ /** Unix timestamp (seconds since epoch). */
183
+ createdAt: number;
184
+ }
133
185
  //#endregion
134
186
  //#region src/resources/courses.d.ts
135
- declare const DEFAULT_LIST_FIELDS: Selection<Course>;
136
- declare const DEFAULT_GET_FIELDS: Selection<Course>;
137
- interface CourseListArgs<S extends Selection<Course> = typeof DEFAULT_LIST_FIELDS> {
187
+ declare const DEFAULT_LIST_FIELDS$1: Selection<Course>;
188
+ declare const DEFAULT_GET_FIELDS$2: Selection<Course>;
189
+ interface CourseListArgs<S extends Selection<Course> = typeof DEFAULT_LIST_FIELDS$1> {
138
190
  limit?: number;
139
191
  page?: number;
140
192
  /** SDK `limit` maps to schema `perPage`. Schema also has its own `limit`
141
193
  * which we expose as `topN` if needed (not in v0). */
142
194
  select?: S;
143
195
  }
144
- interface CourseGetArgs<S extends Selection<Course> = typeof DEFAULT_GET_FIELDS> {
196
+ interface CourseGetArgs<S extends Selection<Course> = typeof DEFAULT_GET_FIELDS$2> {
145
197
  slug: string;
146
198
  select?: S;
147
199
  }
@@ -156,19 +208,120 @@ declare class CoursesResource {
156
208
  * // Opt into GET + Cache-Control hint (60s):
157
209
  * await loopwise.courses.list({ limit: 20 }, { cache: 60 })
158
210
  */
159
- list<S extends Selection<Course> = typeof DEFAULT_LIST_FIELDS>(args?: CourseListArgs<S>, options?: FetchOptions): QueryBuilder<ListResult<Selected<Course, S>>>;
211
+ list<S extends Selection<Course> = typeof DEFAULT_LIST_FIELDS$1>(args?: CourseListArgs<S>, options?: FetchOptions): QueryBuilder<ListResult<Selected<Course, S>>>;
212
+ /**
213
+ * Get a single course by slug.
214
+ *
215
+ * Throws `LoopwiseError("GRAPHQL")` when the slug is not found
216
+ * (Storefront `Queries::Course` raises `service_error("Course-003")`
217
+ * on a `null: false` field). Wrap with try/catch to render 404 routes:
218
+ *
219
+ * @example
220
+ * try {
221
+ * const course = await loopwise.courses.get({ slug: 'finance' })
222
+ * // ...
223
+ * } catch (e) {
224
+ * if (e instanceof LoopwiseError && e.code === 'GRAPHQL') {
225
+ * return Astro.redirect('/404')
226
+ * }
227
+ * throw e
228
+ * }
229
+ */
230
+ get<S extends Selection<Course> = typeof DEFAULT_GET_FIELDS$2>(args: CourseGetArgs<S>, options?: FetchOptions): QueryBuilder<Selected<Course, S>>;
231
+ }
232
+ //#endregion
233
+ //#region src/resources/orders.d.ts
234
+ declare const DEFAULT_GET_FIELDS$1: Selection<Order>;
235
+ interface OrderGetArgs<S extends Selection<Order> = typeof DEFAULT_GET_FIELDS$1> {
236
+ /**
237
+ * Order identifier — the value passed via `?tradeNo=...` on the post-checkout
238
+ * redirect to the merchant's success URL. Internally equals
239
+ * `Payment#trade_no` server-side.
240
+ */
241
+ id: string;
242
+ select?: S;
243
+ }
244
+ declare class OrdersResource {
245
+ private client;
246
+ constructor(client: LoopwiseClient);
247
+ /**
248
+ * Look up a PII-safe order projection by id (the value the storefront
249
+ * appends to a merchant's `checkoutSuccessUrl` as `?tradeNo=...`).
250
+ *
251
+ * Returns `null`-shaped result via the GraphQL `null` payload when the
252
+ * id doesn't resolve in the current tenant — does NOT throw. Use the
253
+ * returned `status` to gate Meta Pixel `Purchase` events:
254
+ *
255
+ * @example
256
+ * const id = new URLSearchParams(location.search).get('tradeNo');
257
+ * if (!id) return;
258
+ * const order = await loopwise.orders.get({ id });
259
+ * if (order?.status === 'PAID') {
260
+ * fbq('track', 'Purchase', {
261
+ * value: order.total,
262
+ * currency: order.currency,
263
+ * contents: order.items.map(i => ({
264
+ * id: i.id, name: i.name, quantity: i.quantity, item_price: i.amount,
265
+ * })),
266
+ * content_ids: order.items.map(i => i.id),
267
+ * content_type: 'product',
268
+ * num_items: order.items.reduce((s, i) => s + i.quantity, 0),
269
+ * }, { eventID: order.id });
270
+ * }
271
+ */
272
+ get<S extends Selection<Order> = typeof DEFAULT_GET_FIELDS$1>(args: OrderGetArgs<S>, options?: FetchOptions): QueryBuilder<Selected<Order, S> | null>;
273
+ }
274
+ //#endregion
275
+ //#region src/resources/posts.d.ts
276
+ declare const DEFAULT_LIST_FIELDS: Selection<Post>;
277
+ declare const DEFAULT_GET_FIELDS: Selection<Post>;
278
+ interface PostListArgs<S extends Selection<Post> = typeof DEFAULT_LIST_FIELDS> {
279
+ limit?: number;
280
+ page?: number;
281
+ /** SDK `limit` maps to schema `perPage`. */
282
+ select?: S;
283
+ }
284
+ interface PostGetArgs<S extends Selection<Post> = typeof DEFAULT_GET_FIELDS> {
285
+ slug: string;
286
+ select?: S;
287
+ }
288
+ declare class PostsResource {
289
+ private client;
290
+ constructor(client: LoopwiseClient);
291
+ /**
292
+ * List published posts for the current tenant, ordered by publishedAt desc.
293
+ * @example
294
+ * const { items, pageInfo } = await loopwise.posts.list({ limit: 10 })
295
+ * @example
296
+ * await loopwise.posts.list({ limit: 10 }, { cache: 60 })
297
+ */
298
+ list<S extends Selection<Post> = typeof DEFAULT_LIST_FIELDS>(args?: PostListArgs<S>, options?: FetchOptions): QueryBuilder<ListResult<Selected<Post, S>>>;
160
299
  /**
161
- * Get a single course by slug. Returns `null` if not found.
300
+ * Get a single post by slug.
301
+ *
302
+ * Throws `LoopwiseError("GRAPHQL")` when the slug is not found
303
+ * (Storefront `Queries::Post` raises `service_error("Post-000")` on a
304
+ * `null: false` field). Wrap with try/catch to render 404 routes:
305
+ *
162
306
  * @example
163
- * const course = await loopwise.courses.get({ slug: 'finance' })
164
- * if (!course) return Astro.redirect('/404')
307
+ * try {
308
+ * const post = await loopwise.posts.get({ slug: 'hello-world' })
309
+ * // ...
310
+ * } catch (e) {
311
+ * if (e instanceof LoopwiseError && e.code === 'GRAPHQL') {
312
+ * return Astro.redirect('/404')
313
+ * }
314
+ * throw e
315
+ * }
165
316
  */
166
- get<S extends Selection<Course> = typeof DEFAULT_GET_FIELDS>(args: CourseGetArgs<S>, options?: FetchOptions): QueryBuilder<Selected<Course, S> | null>;
317
+ get<S extends Selection<Post> = typeof DEFAULT_GET_FIELDS>(args: PostGetArgs<S>, options?: FetchOptions): QueryBuilder<Selected<Post, S>>;
167
318
  }
168
319
  //#endregion
169
320
  //#region src/index.d.ts
170
321
  interface Loopwise {
171
322
  readonly courses: CoursesResource;
323
+ readonly orders: OrdersResource;
324
+ readonly posts: PostsResource;
172
325
  /**
173
326
  * Compose multiple resource queries into a single GraphQL HTTP request.
174
327
  * @example
@@ -190,4 +343,4 @@ interface Loopwise {
190
343
  }
191
344
  declare function createClient(config: LoopwiseConfig): Loopwise;
192
345
  //#endregion
193
- export { type Course, type FetchOptions, type Lecturer, type ListResult, Loopwise, type LoopwiseConfig, LoopwiseError, type PageInfo, QueryBuilder, type Selected, type Selection, createClient };
346
+ export { type Course, type FetchOptions, type Lecturer, type ListResult, Loopwise, type LoopwiseConfig, LoopwiseError, type Order, type OrderItem, type OrderStatus, type PageInfo, type Post, QueryBuilder, type Selected, type Selection, createClient };
package/dist/index.mjs CHANGED
@@ -15,14 +15,12 @@ var LoopwiseError = class extends Error {
15
15
  this.requestId = detail.requestId;
16
16
  }
17
17
  };
18
-
19
18
  //#endregion
20
19
  //#region src/runtime.ts
21
20
  function detectRuntime() {
22
21
  if (typeof window !== "undefined" && typeof window.document !== "undefined") return "browser";
23
22
  return "ssr";
24
23
  }
25
-
26
24
  //#endregion
27
25
  //#region src/client.ts
28
26
  const GET_URL_MAX_LENGTH = 2e3;
@@ -234,7 +232,6 @@ function literal(value) {
234
232
  if (typeof value === "object") return `{ ${Object.entries(value).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}: ${literal(v)}`).join(", ")} }`;
235
233
  throw new LoopwiseError("CONFIG", `Unsupported argument type for GraphQL literal: ${typeof value}`);
236
234
  }
237
-
238
235
  //#endregion
239
236
  //#region src/query.ts
240
237
  var QueryBuilder = class {
@@ -250,7 +247,6 @@ var QueryBuilder = class {
250
247
  return this.fetcher.execute(this.descriptor, this.fetchOptions).then(onFulfilled, onRejected);
251
248
  }
252
249
  };
253
-
254
250
  //#endregion
255
251
  //#region src/schema/courses.ts
256
252
  /** Map SDK Course selection -> schema field selection set. */
@@ -300,16 +296,15 @@ function mapCoursePage(raw) {
300
296
  }
301
297
  };
302
298
  }
303
-
304
299
  //#endregion
305
300
  //#region src/resources/courses.ts
306
- const DEFAULT_LIST_FIELDS = {
301
+ const DEFAULT_LIST_FIELDS$1 = {
307
302
  id: true,
308
303
  slug: true,
309
304
  title: true,
310
305
  coverImage: true
311
306
  };
312
- const DEFAULT_GET_FIELDS = {
307
+ const DEFAULT_GET_FIELDS$2 = {
313
308
  id: true,
314
309
  slug: true,
315
310
  title: true,
@@ -321,13 +316,14 @@ const DEFAULT_GET_FIELDS = {
321
316
  avatar: true
322
317
  }
323
318
  };
324
- function tenantVars(client) {
319
+ function tenantVars$2(client) {
325
320
  return { subdomain: {
326
321
  type: "String!",
327
322
  value: client.config.tenant ?? ""
328
323
  } };
329
324
  }
330
325
  var CoursesResource = class {
326
+ client;
331
327
  constructor(client) {
332
328
  this.client = client;
333
329
  }
@@ -340,7 +336,7 @@ var CoursesResource = class {
340
336
  * await loopwise.courses.list({ limit: 20 }, { cache: 60 })
341
337
  */
342
338
  list(args = {}, options = {}) {
343
- const selectionSet = `nodes { ${buildCourseSelection(args.select ?? DEFAULT_LIST_FIELDS)} } currentPage totalPages nodesCount hasNextPage hasPreviousPage`;
339
+ const selectionSet = `nodes { ${buildCourseSelection(args.select ?? DEFAULT_LIST_FIELDS$1)} } currentPage totalPages nodesCount hasNextPage hasPreviousPage`;
344
340
  return new QueryBuilder(this.client, {
345
341
  operation: "CourseList",
346
342
  field: "courses",
@@ -349,35 +345,291 @@ var CoursesResource = class {
349
345
  page: args.page,
350
346
  perPage: args.limit
351
347
  },
352
- vars: tenantVars(this.client),
348
+ vars: tenantVars$2(this.client),
353
349
  transform: (raw) => mapCoursePage(raw)
354
350
  }, options);
355
351
  }
356
352
  /**
357
- * Get a single course by slug. Returns `null` if not found.
353
+ * Get a single course by slug.
354
+ *
355
+ * Throws `LoopwiseError("GRAPHQL")` when the slug is not found
356
+ * (Storefront `Queries::Course` raises `service_error("Course-003")`
357
+ * on a `null: false` field). Wrap with try/catch to render 404 routes:
358
+ *
358
359
  * @example
359
- * const course = await loopwise.courses.get({ slug: 'finance' })
360
- * if (!course) return Astro.redirect('/404')
360
+ * try {
361
+ * const course = await loopwise.courses.get({ slug: 'finance' })
362
+ * // ...
363
+ * } catch (e) {
364
+ * if (e instanceof LoopwiseError && e.code === 'GRAPHQL') {
365
+ * return Astro.redirect('/404')
366
+ * }
367
+ * throw e
368
+ * }
361
369
  */
362
370
  get(args, options = {}) {
363
- const sel = args.select ?? DEFAULT_GET_FIELDS;
371
+ const sel = args.select ?? DEFAULT_GET_FIELDS$2;
364
372
  return new QueryBuilder(this.client, {
365
373
  operation: "CourseGet",
366
374
  field: "course",
367
375
  selectionSet: buildCourseSelection(sel),
368
376
  args: { slug: args.slug },
377
+ vars: tenantVars$2(this.client),
378
+ transform: (raw) => mapCourse(raw)
379
+ }, options);
380
+ }
381
+ };
382
+ //#endregion
383
+ //#region src/schema/orders.ts
384
+ /** Map SDK Order selection -> schema field selection set. */
385
+ function buildOrderSelection(select) {
386
+ const parts = ["id"];
387
+ const sel = select;
388
+ if (sel.status) parts.push("status");
389
+ if (sel.total) parts.push("total");
390
+ if (sel.currency) parts.push("currency");
391
+ if (sel.createdAt) parts.push("createdAt");
392
+ if (sel.items && typeof sel.items === "object") {
393
+ const itemSel = sel.items;
394
+ const itemParts = ["id"];
395
+ if (itemSel.name) itemParts.push("name");
396
+ if (itemSel.quantity) itemParts.push("quantity");
397
+ if (itemSel.amount) itemParts.push("amount");
398
+ parts.push(`items { ${itemParts.join(" ")} }`);
399
+ }
400
+ return parts.join(" ");
401
+ }
402
+ function mapOrderItem(raw) {
403
+ const out = { id: raw.id };
404
+ if (raw.name !== void 0) out.name = raw.name ?? null;
405
+ if (raw.quantity !== void 0) out.quantity = raw.quantity;
406
+ if (raw.amount !== void 0) out.amount = raw.amount;
407
+ return out;
408
+ }
409
+ function mapOrder(raw) {
410
+ const out = { id: raw.id };
411
+ if (raw.status !== void 0) out.status = raw.status;
412
+ if (raw.total !== void 0) out.total = raw.total;
413
+ if (raw.currency !== void 0) out.currency = raw.currency;
414
+ if (raw.createdAt !== void 0) out.createdAt = raw.createdAt;
415
+ if (raw.items !== void 0) out.items = raw.items.map(mapOrderItem);
416
+ return out;
417
+ }
418
+ //#endregion
419
+ //#region src/resources/orders.ts
420
+ const DEFAULT_GET_FIELDS$1 = {
421
+ id: true,
422
+ status: true,
423
+ total: true,
424
+ currency: true,
425
+ createdAt: true,
426
+ items: {
427
+ id: true,
428
+ name: true,
429
+ quantity: true,
430
+ amount: true
431
+ }
432
+ };
433
+ function tenantVars$1(client) {
434
+ return { subdomain: {
435
+ type: "String!",
436
+ value: client.config.tenant ?? ""
437
+ } };
438
+ }
439
+ var OrdersResource = class {
440
+ client;
441
+ constructor(client) {
442
+ this.client = client;
443
+ }
444
+ /**
445
+ * Look up a PII-safe order projection by id (the value the storefront
446
+ * appends to a merchant's `checkoutSuccessUrl` as `?tradeNo=...`).
447
+ *
448
+ * Returns `null`-shaped result via the GraphQL `null` payload when the
449
+ * id doesn't resolve in the current tenant — does NOT throw. Use the
450
+ * returned `status` to gate Meta Pixel `Purchase` events:
451
+ *
452
+ * @example
453
+ * const id = new URLSearchParams(location.search).get('tradeNo');
454
+ * if (!id) return;
455
+ * const order = await loopwise.orders.get({ id });
456
+ * if (order?.status === 'PAID') {
457
+ * fbq('track', 'Purchase', {
458
+ * value: order.total,
459
+ * currency: order.currency,
460
+ * contents: order.items.map(i => ({
461
+ * id: i.id, name: i.name, quantity: i.quantity, item_price: i.amount,
462
+ * })),
463
+ * content_ids: order.items.map(i => i.id),
464
+ * content_type: 'product',
465
+ * num_items: order.items.reduce((s, i) => s + i.quantity, 0),
466
+ * }, { eventID: order.id });
467
+ * }
468
+ */
469
+ get(args, options = {}) {
470
+ const sel = args.select ?? DEFAULT_GET_FIELDS$1;
471
+ return new QueryBuilder(this.client, {
472
+ operation: "OrderGet",
473
+ field: "order",
474
+ selectionSet: buildOrderSelection(sel),
475
+ args: { id: args.id },
476
+ vars: tenantVars$1(this.client),
477
+ transform: (raw) => raw == null ? null : mapOrder(raw)
478
+ }, options);
479
+ }
480
+ };
481
+ //#endregion
482
+ //#region src/schema/posts.ts
483
+ /** Map SDK Post selection -> schema field selection set. */
484
+ function buildPostSelection(select) {
485
+ const parts = ["id", "slug"];
486
+ const sel = select;
487
+ if (sel.title) parts.push("title");
488
+ if (sel.subtitle) parts.push("subtitle");
489
+ if (sel.excerpt) parts.push("excerpt");
490
+ if (sel.body) parts.push("body");
491
+ if (sel.coverImage) parts.push("coverPhoto");
492
+ if (sel.publishedAt) parts.push("publishedAt");
493
+ if (sel.modifiedAt) parts.push("modifiedAt");
494
+ if (sel.accessType) parts.push("accessType");
495
+ if (sel.tags) parts.push("tags");
496
+ if (sel.noindex) parts.push("noindex");
497
+ if (sel.author && typeof sel.author === "object") {
498
+ const auth = sel.author;
499
+ const authParts = ["id"];
500
+ if (auth.name) authParts.push("name");
501
+ if (auth.avatar) authParts.push("avatar");
502
+ parts.push(`author { ${authParts.join(" ")} }`);
503
+ }
504
+ return parts.join(" ");
505
+ }
506
+ function mapPost(raw) {
507
+ const out = {
508
+ id: raw.id,
509
+ slug: raw.slug
510
+ };
511
+ if (raw.title !== void 0) out.title = raw.title;
512
+ if (raw.subtitle !== void 0) out.subtitle = raw.subtitle ?? null;
513
+ if (raw.excerpt !== void 0) out.excerpt = raw.excerpt ?? null;
514
+ if (raw.body !== void 0) out.body = raw.body;
515
+ if (raw.coverPhoto !== void 0) out.coverImage = raw.coverPhoto ?? null;
516
+ if (raw.publishedAt !== void 0) out.publishedAt = raw.publishedAt;
517
+ if (raw.modifiedAt !== void 0) out.modifiedAt = raw.modifiedAt ?? null;
518
+ if (raw.accessType !== void 0) out.accessType = raw.accessType;
519
+ if (raw.tags !== void 0) out.tags = raw.tags ?? null;
520
+ if (raw.noindex !== void 0) out.noindex = raw.noindex;
521
+ if (raw.author !== void 0) out.author = raw.author ? mapLecturer(raw.author) : null;
522
+ return out;
523
+ }
524
+ function mapPostPage(raw) {
525
+ return {
526
+ items: raw.nodes.map(mapPost),
527
+ pageInfo: {
528
+ page: raw.currentPage,
529
+ totalPages: raw.totalPages,
530
+ totalCount: raw.nodesCount,
531
+ hasNext: raw.hasNextPage,
532
+ hasPrev: raw.hasPreviousPage
533
+ }
534
+ };
535
+ }
536
+ //#endregion
537
+ //#region src/resources/posts.ts
538
+ const DEFAULT_LIST_FIELDS = {
539
+ id: true,
540
+ slug: true,
541
+ title: true,
542
+ excerpt: true,
543
+ coverImage: true,
544
+ publishedAt: true
545
+ };
546
+ const DEFAULT_GET_FIELDS = {
547
+ id: true,
548
+ slug: true,
549
+ title: true,
550
+ subtitle: true,
551
+ excerpt: true,
552
+ body: true,
553
+ coverImage: true,
554
+ publishedAt: true,
555
+ modifiedAt: true,
556
+ accessType: true,
557
+ author: {
558
+ id: true,
559
+ name: true,
560
+ avatar: true
561
+ }
562
+ };
563
+ function tenantVars(client) {
564
+ return { subdomain: {
565
+ type: "String!",
566
+ value: client.config.tenant ?? ""
567
+ } };
568
+ }
569
+ var PostsResource = class {
570
+ client;
571
+ constructor(client) {
572
+ this.client = client;
573
+ }
574
+ /**
575
+ * List published posts for the current tenant, ordered by publishedAt desc.
576
+ * @example
577
+ * const { items, pageInfo } = await loopwise.posts.list({ limit: 10 })
578
+ * @example
579
+ * await loopwise.posts.list({ limit: 10 }, { cache: 60 })
580
+ */
581
+ list(args = {}, options = {}) {
582
+ const selectionSet = `nodes { ${buildPostSelection(args.select ?? DEFAULT_LIST_FIELDS)} } currentPage totalPages nodesCount hasNextPage hasPreviousPage`;
583
+ return new QueryBuilder(this.client, {
584
+ operation: "PostList",
585
+ field: "posts",
586
+ selectionSet,
587
+ args: {
588
+ page: args.page,
589
+ perPage: args.limit
590
+ },
591
+ vars: tenantVars(this.client),
592
+ transform: (raw) => mapPostPage(raw)
593
+ }, options);
594
+ }
595
+ /**
596
+ * Get a single post by slug.
597
+ *
598
+ * Throws `LoopwiseError("GRAPHQL")` when the slug is not found
599
+ * (Storefront `Queries::Post` raises `service_error("Post-000")` on a
600
+ * `null: false` field). Wrap with try/catch to render 404 routes:
601
+ *
602
+ * @example
603
+ * try {
604
+ * const post = await loopwise.posts.get({ slug: 'hello-world' })
605
+ * // ...
606
+ * } catch (e) {
607
+ * if (e instanceof LoopwiseError && e.code === 'GRAPHQL') {
608
+ * return Astro.redirect('/404')
609
+ * }
610
+ * throw e
611
+ * }
612
+ */
613
+ get(args, options = {}) {
614
+ const sel = args.select ?? DEFAULT_GET_FIELDS;
615
+ return new QueryBuilder(this.client, {
616
+ operation: "PostGet",
617
+ field: "post",
618
+ selectionSet: buildPostSelection(sel),
619
+ args: { slug: args.slug },
369
620
  vars: tenantVars(this.client),
370
- transform: (raw) => raw == null ? null : mapCourse(raw)
621
+ transform: (raw) => mapPost(raw)
371
622
  }, options);
372
623
  }
373
624
  };
374
-
375
625
  //#endregion
376
626
  //#region src/index.ts
377
627
  function createClient(config) {
378
628
  const client = new LoopwiseClient(config);
379
629
  return {
380
630
  courses: new CoursesResource(client),
631
+ orders: new OrdersResource(client),
632
+ posts: new PostsResource(client),
381
633
  batch(builders, options) {
382
634
  return client.batch(builders, options);
383
635
  },
@@ -386,6 +638,5 @@ function createClient(config) {
386
638
  }
387
639
  };
388
640
  }
389
-
390
641
  //#endregion
391
- export { LoopwiseError, QueryBuilder, createClient };
642
+ export { LoopwiseError, QueryBuilder, createClient };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loopwise/sdk",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.2",
4
4
  "description": "Loopwise platform SDK — agent-friendly headless API client",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,11 +43,13 @@
43
43
  "build": "tsdown",
44
44
  "dev": "tsdown --watch",
45
45
  "typecheck": "tsc --noEmit",
46
- "test": "vitest run"
46
+ "test": "vitest run",
47
+ "regen:lock": "node scripts/regen-lock.mjs"
47
48
  },
48
49
  "devDependencies": {
50
+ "graphql": "16.13.1",
49
51
  "tsconfig": "workspace:*",
50
- "tsdown": "0.20.3",
52
+ "tsdown": "0.22.0",
51
53
  "typescript": "5.9.3",
52
54
  "vitest": "4.1.0"
53
55
  },