@loopwise/sdk 0.1.0-beta.1 → 0.1.0-beta.3
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/CHANGELOG.md +69 -0
- package/README.md +42 -38
- package/dist/config.d.mts +18 -6
- package/dist/config.mjs +1 -2
- package/dist/index.d.mts +180 -17
- package/dist/index.mjs +290 -34
- package/package.json +7 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@loopwise/sdk` are documented here.
|
|
4
|
+
|
|
5
|
+
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
Versioning: [SemVer](https://semver.org/) with pre-1.0 semantics — minor bumps
|
|
7
|
+
(`0.x → 0.x+1`) may include breaking changes; beta tags (`0.1.0-beta.N`)
|
|
8
|
+
follow the same rules as a single 0.x line.
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [0.1.0-beta.3] — 2026-05-20
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `LoopwiseConfig.site` — preferred name for the site this client reads
|
|
17
|
+
from. Accepts a subdomain (`'acme'`) or full domain
|
|
18
|
+
(`'acme.myteachify.com'`, `'nii.school'`). Replaces the deprecated
|
|
19
|
+
`tenant` field; both fields are accepted, `site` wins when both are set.
|
|
20
|
+
- `LoopwiseError.site` — mirror of the deprecated `tenant` property.
|
|
21
|
+
Catch-and-read consumers can migrate at their own pace.
|
|
22
|
+
|
|
23
|
+
### Deprecated
|
|
24
|
+
|
|
25
|
+
- `LoopwiseConfig.tenant` — renamed to `site`. Both fields still work;
|
|
26
|
+
`site` wins when both are set. **Removed in `0.2.0`.**
|
|
27
|
+
- `LoopwiseError.tenant` — renamed to `site`. **Removed in `0.2.0`.**
|
|
28
|
+
|
|
29
|
+
### Removed
|
|
30
|
+
|
|
31
|
+
- `LoopwiseErrorCode = '…' | 'TENANT_NOT_FOUND'` — the literal was never
|
|
32
|
+
emitted by any SDK code path (it was a reserved/aspirational shape
|
|
33
|
+
that never got runtime mapping). Removed instead of renaming to
|
|
34
|
+
`'SITE_NOT_FOUND'` — no point preserving phantom API surface. If a
|
|
35
|
+
future SDK release adds actual runtime mapping (e.g. translating
|
|
36
|
+
Rails' `School-000` GraphQL error into a typed code), the canonical
|
|
37
|
+
`'SITE_NOT_FOUND'` literal will be added then. Removal is type-only
|
|
38
|
+
(narrowing checks like `case 'TENANT_NOT_FOUND':` were unreachable
|
|
39
|
+
branches in any consumer code).
|
|
40
|
+
|
|
41
|
+
### Changed (non-breaking)
|
|
42
|
+
|
|
43
|
+
- README + every user-visible JSDoc reframed to use "site" language
|
|
44
|
+
instead of "tenant" / "multi-tenant" / "BFF derives tenant from host"
|
|
45
|
+
— SDK consumers don't need to learn the platform's multi-tenancy
|
|
46
|
+
model, just the SDK's behaviour and limits (`createClient({})` reads
|
|
47
|
+
from the page's own site; `createClient({ site: '…' })` reads from a
|
|
48
|
+
different site at the cost of authenticated calls).
|
|
49
|
+
|
|
50
|
+
## [0.1.0-beta.2] — 2026-05-19
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- `orders.get` returns `null` from a null GraphQL payload instead of
|
|
55
|
+
throwing.
|
|
56
|
+
- `orders.get` passes the `id` as an inline GraphQL argument rather than
|
|
57
|
+
a `$variable`, avoiding collision when composed via `batch()`.
|
|
58
|
+
|
|
59
|
+
### Documentation
|
|
60
|
+
|
|
61
|
+
- `orders.get` JSDoc clarifies `OrderItem.amount` is per-unit price
|
|
62
|
+
(not line total), and null-guards the `tradeNo` URL-param lookup
|
|
63
|
+
example.
|
|
64
|
+
|
|
65
|
+
## [0.1.0-beta.1]
|
|
66
|
+
|
|
67
|
+
Initial public beta. `courses.list/get`, `posts.list/get`, `orders.get`,
|
|
68
|
+
`batch()`, raw `query()`. Schema-lock pipeline. Same-origin BFF and
|
|
69
|
+
explicit cross-origin endpoint resolution.
|
package/README.md
CHANGED
|
@@ -15,15 +15,15 @@ Same package powers all three; the difference is the loader.
|
|
|
15
15
|
```ts
|
|
16
16
|
import { createClient } from '@loopwise/sdk';
|
|
17
17
|
|
|
18
|
-
//
|
|
18
|
+
// Reads from the site this page is on. No config needed.
|
|
19
19
|
const loopwise = createClient({});
|
|
20
20
|
const { items } = await loopwise.courses.list({ limit: 20 });
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
That's
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
That's it. The SDK calls the site that's serving the page
|
|
24
|
+
(`acme.kaik.io`, `acme.myteachify.com`, `nii.school`, etc.) and the
|
|
25
|
+
site's API binds the response to itself — you never identify the site
|
|
26
|
+
yourself. To call a different site, see [Calling a different site](#calling-a-different-site).
|
|
27
27
|
|
|
28
28
|
## Quick start — single-file HTML (no build)
|
|
29
29
|
|
|
@@ -32,44 +32,47 @@ response on the trusted server side.
|
|
|
32
32
|
<script type="module">
|
|
33
33
|
import { createClient } from 'https://esm.sh/@loopwise/sdk@beta';
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
// (e.g. acme.kaik.io). Empty config; BFF derives the tenant from host.
|
|
35
|
+
// Reads from the site this HTML is loaded on.
|
|
37
36
|
const loopwise = createClient({});
|
|
38
37
|
const { items } = await loopwise.courses.list({ limit: 20 });
|
|
39
38
|
document.body.textContent = items.map(c => c.title).join(', ');
|
|
40
39
|
</script>
|
|
41
40
|
```
|
|
42
41
|
|
|
43
|
-
##
|
|
42
|
+
## Calling a different site
|
|
44
43
|
|
|
45
|
-
When
|
|
46
|
-
|
|
44
|
+
When your code runs **somewhere other than the site itself** — a Loopwise
|
|
45
|
+
Pages preview at `<scriptName>.loopwise.page`, a build / SSR pipeline,
|
|
46
|
+
server-to-server tooling — name the site you want to read from:
|
|
47
47
|
|
|
48
48
|
```ts
|
|
49
49
|
const loopwise = createClient({
|
|
50
|
-
|
|
51
|
-
apiKey: process.env.LOOPWISE_API_KEY,
|
|
50
|
+
site: 'acme', // 'acme' or 'acme.myteachify.com'
|
|
51
|
+
apiKey: process.env.LOOPWISE_API_KEY, // build/SSR only; browser ignores
|
|
52
52
|
});
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
`site` accepts a subdomain (`'acme'`) or a full domain
|
|
56
|
+
(`'acme.myteachify.com'`, `'nii.school'`). The SDK uses it to pick the
|
|
57
|
+
right API origin.
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
— that is **not** a tenant origin, so:
|
|
59
|
+
### Limits when not on the site itself
|
|
59
60
|
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
Cross-origin requests **do not carry first-party cookies**. So calls
|
|
62
|
+
that require a signed-in visitor — `cart`, `viewer.profile`,
|
|
63
|
+
authenticated mutations — **will not work** from off-site code.
|
|
64
|
+
Public, anonymous reads (`courses.list`, `posts.list`, etc.) work fine.
|
|
65
|
+
|
|
66
|
+
This is the dominant case for `deploy_static_page` bundles (which
|
|
67
|
+
serve at `<scriptName>.loopwise.page`):
|
|
65
68
|
|
|
66
69
|
```ts
|
|
67
70
|
// Inside an HTML bundle deployed to https://lw-acme-home.loopwise.page/
|
|
68
|
-
const loopwise = createClient({
|
|
71
|
+
const loopwise = createClient({ site: 'acme' });
|
|
69
72
|
const { items } = await loopwise.courses.list({ limit: 20 }, { cache: 60 });
|
|
70
73
|
```
|
|
71
74
|
|
|
72
|
-
For preview UX that needs authenticated
|
|
75
|
+
For preview UX that needs an authenticated viewer, see
|
|
73
76
|
[Preview modes](#preview-modes) below.
|
|
74
77
|
|
|
75
78
|
## `cache` option
|
|
@@ -124,22 +127,21 @@ content; use `cache: 'no-store'` for admin previews.
|
|
|
124
127
|
|
|
125
128
|
Two preview surfaces exist, each with different capability:
|
|
126
129
|
|
|
127
|
-
| Surface |
|
|
130
|
+
| Surface | URL | Hosted on the site? | Authenticated viewer? | Use for |
|
|
128
131
|
|---|---|---|---|---|
|
|
129
|
-
| **Loopwise Pages preview** | `<scriptName>.loopwise.page` | ❌
|
|
130
|
-
| **
|
|
132
|
+
| **Loopwise Pages preview** | `<scriptName>.loopwise.page` | ❌ separate origin | ❌ no first-party cookies | Anonymous storefront, marketing pages, public listings |
|
|
133
|
+
| **Site-origin preview** | `<site>/_preview/<scriptName>/` | ✅ same origin as the site | ✅ admins can preview signed in | Cart, checkout, member-gated content, full storefront |
|
|
131
134
|
|
|
132
|
-
The
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
`<TENANT_ORIGIN>` placeholder).
|
|
135
|
+
The site URL can be `<sub>.kaik.io`, `<sub>.myteachify.com`, or a custom
|
|
136
|
+
domain like `nii.school`. `deploy_static_page` returns both URLs in its
|
|
137
|
+
response.
|
|
136
138
|
|
|
137
|
-
Pick the surface that matches what you're testing — anonymous
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
Pick the surface that matches what you're testing — anonymous reads on
|
|
140
|
+
Surface A (faster, edge-cacheable), authenticated flows on Surface B
|
|
141
|
+
(signed-in only, never edge-cached). Surface B requires relative asset
|
|
142
|
+
paths in your bundle (the proxy injects `<base>` so relative paths
|
|
143
|
+
resolve under `/_preview/<scriptName>/`; root-absolute paths like
|
|
144
|
+
`/style.css` still go to the site root and 404).
|
|
143
145
|
|
|
144
146
|
## React — coming soon
|
|
145
147
|
|
|
@@ -155,10 +157,12 @@ your own state (`useEffect` + `useState`, TanStack Query, etc.).
|
|
|
155
157
|
|
|
156
158
|
## Public API
|
|
157
159
|
|
|
158
|
-
- `createClient(config)` — storefront client. `config.
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
- `createClient(config)` — storefront client. Pass `config.site` only
|
|
161
|
+
when calling a different site than the one your code is running on
|
|
162
|
+
(see [Calling a different site](#calling-a-different-site)).
|
|
161
163
|
- `loopwise.courses.list(args, options?)` / `.get(args, options?)`.
|
|
164
|
+
- `loopwise.posts.list(args, options?)` / `.get(args, options?)`.
|
|
165
|
+
- `loopwise.orders.get(args, options?)` — post-checkout pixel-tracking pattern; see the method's JSDoc for the `?tradeNo=…` → `fbq('track', 'Purchase', …)` recipe.
|
|
162
166
|
- `loopwise.batch(builders, options?)` — composes N queries into one HTTP request.
|
|
163
167
|
- `loopwise.query<T>(document, variables?, operationName?, options?)` — raw GraphQL escape hatch.
|
|
164
168
|
|
package/dist/config.d.mts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
//#region src/config.d.ts
|
|
2
2
|
interface LoopwiseConfig {
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* The site this client reads from. Accepts a subdomain (`'acme'`) or a
|
|
5
|
+
* full domain (`'acme.myteachify.com'`, `'nii.school'`).
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* Omit when your code runs on the site itself — the SDK calls the
|
|
8
|
+
* page's own origin. Required when running off-origin: cross-domain
|
|
9
|
+
* embeds (Loopwise Pages previews at `*.loopwise.page`), SSR / build
|
|
10
|
+
* pipelines, server-to-server tooling.
|
|
11
|
+
*
|
|
12
|
+
* Cross-origin requests carry no first-party cookies, so authenticated
|
|
13
|
+
* calls (`cart`, `viewer.profile`, mutations that need a signed-in
|
|
14
|
+
* visitor) won't work in that mode — public reads do.
|
|
15
|
+
*/
|
|
16
|
+
site?: string;
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated Renamed to `site`. Both fields work for now; if both are
|
|
19
|
+
* set, `site` wins. **Will be removed in `0.2.0`.**
|
|
8
20
|
*/
|
|
9
21
|
tenant?: string;
|
|
10
22
|
/**
|
|
@@ -15,9 +27,9 @@ interface LoopwiseConfig {
|
|
|
15
27
|
*/
|
|
16
28
|
apiKey?: string;
|
|
17
29
|
/**
|
|
18
|
-
* Override the GraphQL endpoint.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
30
|
+
* Override the GraphQL endpoint. You normally never set this — the SDK
|
|
31
|
+
* picks the right URL from `site` (or the page's own origin when `site`
|
|
32
|
+
* is omitted).
|
|
21
33
|
*/
|
|
22
34
|
endpoint?: string;
|
|
23
35
|
}
|
package/dist/config.mjs
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LoopwiseConfig } from "./config.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/error.d.ts
|
|
4
|
-
type LoopwiseErrorCode = 'NETWORK' | 'AUTH' | 'GRAPHQL' | 'RATE_LIMIT' | '
|
|
4
|
+
type LoopwiseErrorCode = 'NETWORK' | 'AUTH' | 'GRAPHQL' | 'RATE_LIMIT' | 'CONFIG';
|
|
5
5
|
interface GraphQLError {
|
|
6
6
|
message: string;
|
|
7
7
|
path?: (string | number)[];
|
|
@@ -9,11 +9,15 @@ interface GraphQLError {
|
|
|
9
9
|
}
|
|
10
10
|
declare class LoopwiseError extends Error {
|
|
11
11
|
readonly code: LoopwiseErrorCode;
|
|
12
|
+
/** The site this client was configured for. */
|
|
13
|
+
readonly site?: string;
|
|
14
|
+
/** @deprecated Renamed to `site`. Holds the same resolved value. Removed in 0.2.0. */
|
|
12
15
|
readonly tenant?: string;
|
|
13
16
|
readonly endpoint?: string;
|
|
14
17
|
readonly graphqlErrors?: GraphQLError[];
|
|
15
18
|
readonly requestId?: string;
|
|
16
19
|
constructor(code: LoopwiseErrorCode, message: string, detail?: {
|
|
20
|
+
site?: string; /** @deprecated Renamed to `site`. Removed in 0.2.0. */
|
|
17
21
|
tenant?: string;
|
|
18
22
|
endpoint?: string;
|
|
19
23
|
graphqlErrors?: GraphQLError[];
|
|
@@ -79,9 +83,7 @@ declare class QueryBuilder<T> implements PromiseLike<T> {
|
|
|
79
83
|
//#endregion
|
|
80
84
|
//#region src/client.d.ts
|
|
81
85
|
declare class LoopwiseClient implements BatchedFetcher {
|
|
82
|
-
readonly config:
|
|
83
|
-
tenant?: string;
|
|
84
|
-
} & LoopwiseConfig;
|
|
86
|
+
readonly config: LoopwiseConfig;
|
|
85
87
|
private endpoint;
|
|
86
88
|
constructor(config: LoopwiseConfig);
|
|
87
89
|
private defaultEndpoint;
|
|
@@ -119,6 +121,34 @@ interface Lecturer {
|
|
|
119
121
|
name: string;
|
|
120
122
|
avatar: string | null;
|
|
121
123
|
}
|
|
124
|
+
interface Post {
|
|
125
|
+
id: string;
|
|
126
|
+
slug: string;
|
|
127
|
+
title: string;
|
|
128
|
+
subtitle: string | null;
|
|
129
|
+
excerpt: string | null;
|
|
130
|
+
/** Full body HTML. Truncated server-side when access is gated and the
|
|
131
|
+
* viewer doesn't have permission (login_required without auth, or paid
|
|
132
|
+
* without an active subscription). */
|
|
133
|
+
body: string;
|
|
134
|
+
/** Maps to schema `Post.cover_photo`. */
|
|
135
|
+
coverImage: string | null;
|
|
136
|
+
/** Unix timestamp (seconds since epoch). */
|
|
137
|
+
publishedAt: number;
|
|
138
|
+
/** Unix timestamp of the last update — schema sources this from the
|
|
139
|
+
* post's `updated_at`, so for any persisted record this is populated
|
|
140
|
+
* (typically equal to or after `publishedAt`). The `null` is included
|
|
141
|
+
* for forward-compat with a hypothetical "never modified" sentinel
|
|
142
|
+
* but isn't observed in current production data. */
|
|
143
|
+
modifiedAt: number | null;
|
|
144
|
+
/** Known values: `login_required`, `paid`, `public_access`. The
|
|
145
|
+
* `string & {}` escape hatch keeps autocomplete on the known set
|
|
146
|
+
* while accepting forward-compat values the schema may add. */
|
|
147
|
+
accessType: 'login_required' | 'paid' | 'public_access' | (string & {});
|
|
148
|
+
tags: string[] | null;
|
|
149
|
+
noindex: boolean;
|
|
150
|
+
author: Lecturer | null;
|
|
151
|
+
}
|
|
122
152
|
interface PageInfo {
|
|
123
153
|
page: number;
|
|
124
154
|
totalPages: number;
|
|
@@ -130,18 +160,42 @@ interface ListResult<T> {
|
|
|
130
160
|
items: T[];
|
|
131
161
|
pageInfo: PageInfo;
|
|
132
162
|
}
|
|
163
|
+
/** Simplified order status. Mirrors the storefront `Order.status` enum. */
|
|
164
|
+
type OrderStatus = 'PAID' | 'PENDING' | 'FAILED' | 'REFUNDED';
|
|
165
|
+
interface OrderItem {
|
|
166
|
+
/**
|
|
167
|
+
* Meta-pixel-aligned identifier (Lineitem#tracking_id server-side).
|
|
168
|
+
* Use directly in `content_ids` / `contents[].id` for Purchase events.
|
|
169
|
+
*/
|
|
170
|
+
id: string;
|
|
171
|
+
name: string | null;
|
|
172
|
+
quantity: number;
|
|
173
|
+
/** Per-unit price in the order currency. Maps to FB Pixel `contents[].item_price`. */
|
|
174
|
+
amount: number;
|
|
175
|
+
}
|
|
176
|
+
interface Order {
|
|
177
|
+
/** Stable order identifier — currently equals `Payment#trade_no` server-side. */
|
|
178
|
+
id: string;
|
|
179
|
+
status: OrderStatus;
|
|
180
|
+
/** Total paid amount in the order currency. Maps to FB Pixel `value`. */
|
|
181
|
+
total: number;
|
|
182
|
+
currency: string;
|
|
183
|
+
items: OrderItem[];
|
|
184
|
+
/** Unix timestamp (seconds since epoch). */
|
|
185
|
+
createdAt: number;
|
|
186
|
+
}
|
|
133
187
|
//#endregion
|
|
134
188
|
//#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> {
|
|
189
|
+
declare const DEFAULT_LIST_FIELDS$1: Selection<Course>;
|
|
190
|
+
declare const DEFAULT_GET_FIELDS$2: Selection<Course>;
|
|
191
|
+
interface CourseListArgs<S extends Selection<Course> = typeof DEFAULT_LIST_FIELDS$1> {
|
|
138
192
|
limit?: number;
|
|
139
193
|
page?: number;
|
|
140
194
|
/** SDK `limit` maps to schema `perPage`. Schema also has its own `limit`
|
|
141
195
|
* which we expose as `topN` if needed (not in v0). */
|
|
142
196
|
select?: S;
|
|
143
197
|
}
|
|
144
|
-
interface CourseGetArgs<S extends Selection<Course> = typeof DEFAULT_GET_FIELDS> {
|
|
198
|
+
interface CourseGetArgs<S extends Selection<Course> = typeof DEFAULT_GET_FIELDS$2> {
|
|
145
199
|
slug: string;
|
|
146
200
|
select?: S;
|
|
147
201
|
}
|
|
@@ -149,26 +203,126 @@ declare class CoursesResource {
|
|
|
149
203
|
private client;
|
|
150
204
|
constructor(client: LoopwiseClient);
|
|
151
205
|
/**
|
|
152
|
-
* List courses
|
|
206
|
+
* List published courses.
|
|
153
207
|
* @example
|
|
154
208
|
* const { items, pageInfo } = await loopwise.courses.list({ limit: 20 })
|
|
155
209
|
* @example
|
|
156
210
|
* // Opt into GET + Cache-Control hint (60s):
|
|
157
211
|
* await loopwise.courses.list({ limit: 20 }, { cache: 60 })
|
|
158
212
|
*/
|
|
159
|
-
list<S extends Selection<Course> = typeof DEFAULT_LIST_FIELDS>(args?: CourseListArgs<S>, options?: FetchOptions): QueryBuilder<ListResult<Selected<Course, S>>>;
|
|
213
|
+
list<S extends Selection<Course> = typeof DEFAULT_LIST_FIELDS$1>(args?: CourseListArgs<S>, options?: FetchOptions): QueryBuilder<ListResult<Selected<Course, S>>>;
|
|
214
|
+
/**
|
|
215
|
+
* Get a single course by slug.
|
|
216
|
+
*
|
|
217
|
+
* Throws `LoopwiseError("GRAPHQL")` when the slug is not found
|
|
218
|
+
* (Storefront `Queries::Course` raises `service_error("Course-003")`
|
|
219
|
+
* on a `null: false` field). Wrap with try/catch to render 404 routes:
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* try {
|
|
223
|
+
* const course = await loopwise.courses.get({ slug: 'finance' })
|
|
224
|
+
* // ...
|
|
225
|
+
* } catch (e) {
|
|
226
|
+
* if (e instanceof LoopwiseError && e.code === 'GRAPHQL') {
|
|
227
|
+
* return Astro.redirect('/404')
|
|
228
|
+
* }
|
|
229
|
+
* throw e
|
|
230
|
+
* }
|
|
231
|
+
*/
|
|
232
|
+
get<S extends Selection<Course> = typeof DEFAULT_GET_FIELDS$2>(args: CourseGetArgs<S>, options?: FetchOptions): QueryBuilder<Selected<Course, S>>;
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/resources/orders.d.ts
|
|
236
|
+
declare const DEFAULT_GET_FIELDS$1: Selection<Order>;
|
|
237
|
+
interface OrderGetArgs<S extends Selection<Order> = typeof DEFAULT_GET_FIELDS$1> {
|
|
238
|
+
/**
|
|
239
|
+
* Order identifier — the value passed via `?tradeNo=...` on the post-checkout
|
|
240
|
+
* redirect to the merchant's success URL. Internally equals
|
|
241
|
+
* `Payment#trade_no` server-side.
|
|
242
|
+
*/
|
|
243
|
+
id: string;
|
|
244
|
+
select?: S;
|
|
245
|
+
}
|
|
246
|
+
declare class OrdersResource {
|
|
247
|
+
private client;
|
|
248
|
+
constructor(client: LoopwiseClient);
|
|
249
|
+
/**
|
|
250
|
+
* Look up a PII-safe order projection by id (the value the storefront
|
|
251
|
+
* appends to a merchant's `checkoutSuccessUrl` as `?tradeNo=...`).
|
|
252
|
+
*
|
|
253
|
+
* Returns `null` when the id doesn't resolve on this site — does NOT
|
|
254
|
+
* throw. Use the returned `status` to gate Meta Pixel `Purchase` events:
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* const id = new URLSearchParams(location.search).get('tradeNo');
|
|
258
|
+
* if (!id) return;
|
|
259
|
+
* const order = await loopwise.orders.get({ id });
|
|
260
|
+
* if (order?.status === 'PAID') {
|
|
261
|
+
* fbq('track', 'Purchase', {
|
|
262
|
+
* value: order.total,
|
|
263
|
+
* currency: order.currency,
|
|
264
|
+
* contents: order.items.map(i => ({
|
|
265
|
+
* id: i.id, name: i.name, quantity: i.quantity, item_price: i.amount,
|
|
266
|
+
* })),
|
|
267
|
+
* content_ids: order.items.map(i => i.id),
|
|
268
|
+
* content_type: 'product',
|
|
269
|
+
* num_items: order.items.reduce((s, i) => s + i.quantity, 0),
|
|
270
|
+
* }, { eventID: order.id });
|
|
271
|
+
* }
|
|
272
|
+
*/
|
|
273
|
+
get<S extends Selection<Order> = typeof DEFAULT_GET_FIELDS$1>(args: OrderGetArgs<S>, options?: FetchOptions): QueryBuilder<Selected<Order, S> | null>;
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/resources/posts.d.ts
|
|
277
|
+
declare const DEFAULT_LIST_FIELDS: Selection<Post>;
|
|
278
|
+
declare const DEFAULT_GET_FIELDS: Selection<Post>;
|
|
279
|
+
interface PostListArgs<S extends Selection<Post> = typeof DEFAULT_LIST_FIELDS> {
|
|
280
|
+
limit?: number;
|
|
281
|
+
page?: number;
|
|
282
|
+
/** SDK `limit` maps to schema `perPage`. */
|
|
283
|
+
select?: S;
|
|
284
|
+
}
|
|
285
|
+
interface PostGetArgs<S extends Selection<Post> = typeof DEFAULT_GET_FIELDS> {
|
|
286
|
+
slug: string;
|
|
287
|
+
select?: S;
|
|
288
|
+
}
|
|
289
|
+
declare class PostsResource {
|
|
290
|
+
private client;
|
|
291
|
+
constructor(client: LoopwiseClient);
|
|
292
|
+
/**
|
|
293
|
+
* List published posts, ordered by publishedAt desc.
|
|
294
|
+
* @example
|
|
295
|
+
* const { items, pageInfo } = await loopwise.posts.list({ limit: 10 })
|
|
296
|
+
* @example
|
|
297
|
+
* await loopwise.posts.list({ limit: 10 }, { cache: 60 })
|
|
298
|
+
*/
|
|
299
|
+
list<S extends Selection<Post> = typeof DEFAULT_LIST_FIELDS>(args?: PostListArgs<S>, options?: FetchOptions): QueryBuilder<ListResult<Selected<Post, S>>>;
|
|
160
300
|
/**
|
|
161
|
-
* Get a single
|
|
301
|
+
* Get a single post by slug.
|
|
302
|
+
*
|
|
303
|
+
* Throws `LoopwiseError("GRAPHQL")` when the slug is not found
|
|
304
|
+
* (Storefront `Queries::Post` raises `service_error("Post-000")` on a
|
|
305
|
+
* `null: false` field). Wrap with try/catch to render 404 routes:
|
|
306
|
+
*
|
|
162
307
|
* @example
|
|
163
|
-
*
|
|
164
|
-
*
|
|
308
|
+
* try {
|
|
309
|
+
* const post = await loopwise.posts.get({ slug: 'hello-world' })
|
|
310
|
+
* // ...
|
|
311
|
+
* } catch (e) {
|
|
312
|
+
* if (e instanceof LoopwiseError && e.code === 'GRAPHQL') {
|
|
313
|
+
* return Astro.redirect('/404')
|
|
314
|
+
* }
|
|
315
|
+
* throw e
|
|
316
|
+
* }
|
|
165
317
|
*/
|
|
166
|
-
get<S extends Selection<
|
|
318
|
+
get<S extends Selection<Post> = typeof DEFAULT_GET_FIELDS>(args: PostGetArgs<S>, options?: FetchOptions): QueryBuilder<Selected<Post, S>>;
|
|
167
319
|
}
|
|
168
320
|
//#endregion
|
|
169
321
|
//#region src/index.d.ts
|
|
170
322
|
interface Loopwise {
|
|
171
323
|
readonly courses: CoursesResource;
|
|
324
|
+
readonly orders: OrdersResource;
|
|
325
|
+
readonly posts: PostsResource;
|
|
172
326
|
/**
|
|
173
327
|
* Compose multiple resource queries into a single GraphQL HTTP request.
|
|
174
328
|
* @example
|
|
@@ -181,13 +335,22 @@ interface Loopwise {
|
|
|
181
335
|
/**
|
|
182
336
|
* Raw GraphQL escape hatch. Use when a typed resource method doesn't cover
|
|
183
337
|
* what you need (new schema fields, custom queries, ad-hoc inspection).
|
|
338
|
+
* Variables are sent as-is; errors are still mapped to LoopwiseError.
|
|
339
|
+
*
|
|
340
|
+
* Most storefront fields (`courses`, `posts`, `order`, ...) take a
|
|
341
|
+
* required `subdomain: String!` argument. The BFF rewrites that variable
|
|
342
|
+
* to the trusted server-derived value on every request, so consumers
|
|
343
|
+
* declaring `$subdomain` see whatever value the BFF binds — not what
|
|
344
|
+
* they pass.
|
|
345
|
+
*
|
|
184
346
|
* @example
|
|
185
|
-
*
|
|
186
|
-
*
|
|
347
|
+
* // Minimal shape demo (always valid, no scoping required):
|
|
348
|
+
* const data = await loopwise.query<{ __typename: string }>(`
|
|
349
|
+
* query { __typename }
|
|
187
350
|
* `)
|
|
188
351
|
*/
|
|
189
352
|
query<T = unknown>(document: string, variables?: Record<string, unknown>, operationName?: string, options?: FetchOptions): Promise<T>;
|
|
190
353
|
}
|
|
191
354
|
declare function createClient(config: LoopwiseConfig): Loopwise;
|
|
192
355
|
//#endregion
|
|
193
|
-
export { type Course, type FetchOptions, type Lecturer, type ListResult, Loopwise, type LoopwiseConfig, LoopwiseError, type PageInfo, QueryBuilder, type Selected, type Selection, createClient };
|
|
356
|
+
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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
//#region src/error.ts
|
|
2
2
|
var LoopwiseError = class extends Error {
|
|
3
3
|
code;
|
|
4
|
+
/** The site this client was configured for. */
|
|
5
|
+
site;
|
|
6
|
+
/** @deprecated Renamed to `site`. Holds the same resolved value. Removed in 0.2.0. */
|
|
4
7
|
tenant;
|
|
5
8
|
endpoint;
|
|
6
9
|
graphqlErrors;
|
|
@@ -9,20 +12,20 @@ var LoopwiseError = class extends Error {
|
|
|
9
12
|
super(message);
|
|
10
13
|
this.name = "LoopwiseError";
|
|
11
14
|
this.code = code;
|
|
12
|
-
|
|
15
|
+
const site = detail.site ?? detail.tenant;
|
|
16
|
+
this.site = site;
|
|
17
|
+
this.tenant = site;
|
|
13
18
|
this.endpoint = detail.endpoint;
|
|
14
19
|
this.graphqlErrors = detail.graphqlErrors;
|
|
15
20
|
this.requestId = detail.requestId;
|
|
16
21
|
}
|
|
17
22
|
};
|
|
18
|
-
|
|
19
23
|
//#endregion
|
|
20
24
|
//#region src/runtime.ts
|
|
21
25
|
function detectRuntime() {
|
|
22
26
|
if (typeof window !== "undefined" && typeof window.document !== "undefined") return "browser";
|
|
23
27
|
return "ssr";
|
|
24
28
|
}
|
|
25
|
-
|
|
26
29
|
//#endregion
|
|
27
30
|
//#region src/client.ts
|
|
28
31
|
const GET_URL_MAX_LENGTH = 2e3;
|
|
@@ -31,20 +34,21 @@ var LoopwiseClient = class {
|
|
|
31
34
|
config;
|
|
32
35
|
endpoint;
|
|
33
36
|
constructor(config) {
|
|
34
|
-
const
|
|
35
|
-
if (detectRuntime() !== "browser" && !
|
|
37
|
+
const site = config.site ?? config.tenant;
|
|
38
|
+
if (detectRuntime() !== "browser" && !site) throw new LoopwiseError("CONFIG", "site is required outside browser runtime. Pass it to createClient({ site }).");
|
|
36
39
|
this.config = {
|
|
37
40
|
...config,
|
|
38
|
-
|
|
41
|
+
site,
|
|
42
|
+
tenant: site
|
|
39
43
|
};
|
|
40
44
|
this.endpoint = config.endpoint ?? this.defaultEndpoint();
|
|
41
45
|
}
|
|
42
46
|
defaultEndpoint() {
|
|
43
|
-
const
|
|
44
|
-
if (detectRuntime() === "browser" && !
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
return `https://${
|
|
47
|
+
const site = this.config.site;
|
|
48
|
+
if (detectRuntime() === "browser" && !site) return "/api/graphql";
|
|
49
|
+
const s = site;
|
|
50
|
+
if (s.includes(".")) return `https://${s}/api/graphql`;
|
|
51
|
+
return `https://${s}.kaik.io/api/graphql`;
|
|
48
52
|
}
|
|
49
53
|
async execute(descriptor, options = {}) {
|
|
50
54
|
const document = renderSingleDocument(descriptor);
|
|
@@ -117,26 +121,26 @@ var LoopwiseClient = class {
|
|
|
117
121
|
});
|
|
118
122
|
} catch (err) {
|
|
119
123
|
throw new LoopwiseError("NETWORK", `Network error: ${err.message}`, {
|
|
120
|
-
|
|
124
|
+
site: this.config.site,
|
|
121
125
|
endpoint: this.endpoint
|
|
122
126
|
});
|
|
123
127
|
}
|
|
124
128
|
if (response.status === 401 || response.status === 403) throw new LoopwiseError("AUTH", `Auth failed (${response.status})`, {
|
|
125
|
-
|
|
129
|
+
site: this.config.site,
|
|
126
130
|
endpoint: this.endpoint,
|
|
127
131
|
requestId: response.headers.get("x-request-id") ?? void 0
|
|
128
132
|
});
|
|
129
133
|
if (response.status === 429) throw new LoopwiseError("RATE_LIMIT", "Rate limited", {
|
|
130
|
-
|
|
134
|
+
site: this.config.site,
|
|
131
135
|
endpoint: this.endpoint
|
|
132
136
|
});
|
|
133
137
|
if (!response.ok) throw new LoopwiseError("NETWORK", `Upstream ${response.status}`, {
|
|
134
|
-
|
|
138
|
+
site: this.config.site,
|
|
135
139
|
endpoint: this.endpoint
|
|
136
140
|
});
|
|
137
141
|
const body = await response.json();
|
|
138
142
|
if (body.errors?.length) throw new LoopwiseError("GRAPHQL", body.errors[0]?.message ?? "GraphQL error", {
|
|
139
|
-
|
|
143
|
+
site: this.config.site,
|
|
140
144
|
endpoint: this.endpoint,
|
|
141
145
|
graphqlErrors: body.errors
|
|
142
146
|
});
|
|
@@ -234,7 +238,6 @@ function literal(value) {
|
|
|
234
238
|
if (typeof value === "object") return `{ ${Object.entries(value).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}: ${literal(v)}`).join(", ")} }`;
|
|
235
239
|
throw new LoopwiseError("CONFIG", `Unsupported argument type for GraphQL literal: ${typeof value}`);
|
|
236
240
|
}
|
|
237
|
-
|
|
238
241
|
//#endregion
|
|
239
242
|
//#region src/query.ts
|
|
240
243
|
var QueryBuilder = class {
|
|
@@ -250,7 +253,6 @@ var QueryBuilder = class {
|
|
|
250
253
|
return this.fetcher.execute(this.descriptor, this.fetchOptions).then(onFulfilled, onRejected);
|
|
251
254
|
}
|
|
252
255
|
};
|
|
253
|
-
|
|
254
256
|
//#endregion
|
|
255
257
|
//#region src/schema/courses.ts
|
|
256
258
|
/** Map SDK Course selection -> schema field selection set. */
|
|
@@ -300,16 +302,15 @@ function mapCoursePage(raw) {
|
|
|
300
302
|
}
|
|
301
303
|
};
|
|
302
304
|
}
|
|
303
|
-
|
|
304
305
|
//#endregion
|
|
305
306
|
//#region src/resources/courses.ts
|
|
306
|
-
const DEFAULT_LIST_FIELDS = {
|
|
307
|
+
const DEFAULT_LIST_FIELDS$1 = {
|
|
307
308
|
id: true,
|
|
308
309
|
slug: true,
|
|
309
310
|
title: true,
|
|
310
311
|
coverImage: true
|
|
311
312
|
};
|
|
312
|
-
const DEFAULT_GET_FIELDS = {
|
|
313
|
+
const DEFAULT_GET_FIELDS$2 = {
|
|
313
314
|
id: true,
|
|
314
315
|
slug: true,
|
|
315
316
|
title: true,
|
|
@@ -321,18 +322,19 @@ const DEFAULT_GET_FIELDS = {
|
|
|
321
322
|
avatar: true
|
|
322
323
|
}
|
|
323
324
|
};
|
|
324
|
-
function tenantVars(client) {
|
|
325
|
+
function tenantVars$2(client) {
|
|
325
326
|
return { subdomain: {
|
|
326
327
|
type: "String!",
|
|
327
|
-
value: client.config.
|
|
328
|
+
value: client.config.site ?? ""
|
|
328
329
|
} };
|
|
329
330
|
}
|
|
330
331
|
var CoursesResource = class {
|
|
332
|
+
client;
|
|
331
333
|
constructor(client) {
|
|
332
334
|
this.client = client;
|
|
333
335
|
}
|
|
334
336
|
/**
|
|
335
|
-
* List courses
|
|
337
|
+
* List published courses.
|
|
336
338
|
* @example
|
|
337
339
|
* const { items, pageInfo } = await loopwise.courses.list({ limit: 20 })
|
|
338
340
|
* @example
|
|
@@ -340,7 +342,7 @@ var CoursesResource = class {
|
|
|
340
342
|
* await loopwise.courses.list({ limit: 20 }, { cache: 60 })
|
|
341
343
|
*/
|
|
342
344
|
list(args = {}, options = {}) {
|
|
343
|
-
const selectionSet = `nodes { ${buildCourseSelection(args.select ?? DEFAULT_LIST_FIELDS)} } currentPage totalPages nodesCount hasNextPage hasPreviousPage`;
|
|
345
|
+
const selectionSet = `nodes { ${buildCourseSelection(args.select ?? DEFAULT_LIST_FIELDS$1)} } currentPage totalPages nodesCount hasNextPage hasPreviousPage`;
|
|
344
346
|
return new QueryBuilder(this.client, {
|
|
345
347
|
operation: "CourseList",
|
|
346
348
|
field: "courses",
|
|
@@ -349,35 +351,290 @@ var CoursesResource = class {
|
|
|
349
351
|
page: args.page,
|
|
350
352
|
perPage: args.limit
|
|
351
353
|
},
|
|
352
|
-
vars: tenantVars(this.client),
|
|
354
|
+
vars: tenantVars$2(this.client),
|
|
353
355
|
transform: (raw) => mapCoursePage(raw)
|
|
354
356
|
}, options);
|
|
355
357
|
}
|
|
356
358
|
/**
|
|
357
|
-
* Get a single course by slug.
|
|
359
|
+
* Get a single course by slug.
|
|
360
|
+
*
|
|
361
|
+
* Throws `LoopwiseError("GRAPHQL")` when the slug is not found
|
|
362
|
+
* (Storefront `Queries::Course` raises `service_error("Course-003")`
|
|
363
|
+
* on a `null: false` field). Wrap with try/catch to render 404 routes:
|
|
364
|
+
*
|
|
358
365
|
* @example
|
|
359
|
-
*
|
|
360
|
-
*
|
|
366
|
+
* try {
|
|
367
|
+
* const course = await loopwise.courses.get({ slug: 'finance' })
|
|
368
|
+
* // ...
|
|
369
|
+
* } catch (e) {
|
|
370
|
+
* if (e instanceof LoopwiseError && e.code === 'GRAPHQL') {
|
|
371
|
+
* return Astro.redirect('/404')
|
|
372
|
+
* }
|
|
373
|
+
* throw e
|
|
374
|
+
* }
|
|
361
375
|
*/
|
|
362
376
|
get(args, options = {}) {
|
|
363
|
-
const sel = args.select ?? DEFAULT_GET_FIELDS;
|
|
377
|
+
const sel = args.select ?? DEFAULT_GET_FIELDS$2;
|
|
364
378
|
return new QueryBuilder(this.client, {
|
|
365
379
|
operation: "CourseGet",
|
|
366
380
|
field: "course",
|
|
367
381
|
selectionSet: buildCourseSelection(sel),
|
|
368
382
|
args: { slug: args.slug },
|
|
383
|
+
vars: tenantVars$2(this.client),
|
|
384
|
+
transform: (raw) => mapCourse(raw)
|
|
385
|
+
}, options);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/schema/orders.ts
|
|
390
|
+
/** Map SDK Order selection -> schema field selection set. */
|
|
391
|
+
function buildOrderSelection(select) {
|
|
392
|
+
const parts = ["id"];
|
|
393
|
+
const sel = select;
|
|
394
|
+
if (sel.status) parts.push("status");
|
|
395
|
+
if (sel.total) parts.push("total");
|
|
396
|
+
if (sel.currency) parts.push("currency");
|
|
397
|
+
if (sel.createdAt) parts.push("createdAt");
|
|
398
|
+
if (sel.items && typeof sel.items === "object") {
|
|
399
|
+
const itemSel = sel.items;
|
|
400
|
+
const itemParts = ["id"];
|
|
401
|
+
if (itemSel.name) itemParts.push("name");
|
|
402
|
+
if (itemSel.quantity) itemParts.push("quantity");
|
|
403
|
+
if (itemSel.amount) itemParts.push("amount");
|
|
404
|
+
parts.push(`items { ${itemParts.join(" ")} }`);
|
|
405
|
+
}
|
|
406
|
+
return parts.join(" ");
|
|
407
|
+
}
|
|
408
|
+
function mapOrderItem(raw) {
|
|
409
|
+
const out = { id: raw.id };
|
|
410
|
+
if (raw.name !== void 0) out.name = raw.name ?? null;
|
|
411
|
+
if (raw.quantity !== void 0) out.quantity = raw.quantity;
|
|
412
|
+
if (raw.amount !== void 0) out.amount = raw.amount;
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
function mapOrder(raw) {
|
|
416
|
+
const out = { id: raw.id };
|
|
417
|
+
if (raw.status !== void 0) out.status = raw.status;
|
|
418
|
+
if (raw.total !== void 0) out.total = raw.total;
|
|
419
|
+
if (raw.currency !== void 0) out.currency = raw.currency;
|
|
420
|
+
if (raw.createdAt !== void 0) out.createdAt = raw.createdAt;
|
|
421
|
+
if (raw.items !== void 0) out.items = raw.items.map(mapOrderItem);
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
425
|
+
//#region src/resources/orders.ts
|
|
426
|
+
const DEFAULT_GET_FIELDS$1 = {
|
|
427
|
+
id: true,
|
|
428
|
+
status: true,
|
|
429
|
+
total: true,
|
|
430
|
+
currency: true,
|
|
431
|
+
createdAt: true,
|
|
432
|
+
items: {
|
|
433
|
+
id: true,
|
|
434
|
+
name: true,
|
|
435
|
+
quantity: true,
|
|
436
|
+
amount: true
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
function tenantVars$1(client) {
|
|
440
|
+
return { subdomain: {
|
|
441
|
+
type: "String!",
|
|
442
|
+
value: client.config.site ?? ""
|
|
443
|
+
} };
|
|
444
|
+
}
|
|
445
|
+
var OrdersResource = class {
|
|
446
|
+
client;
|
|
447
|
+
constructor(client) {
|
|
448
|
+
this.client = client;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Look up a PII-safe order projection by id (the value the storefront
|
|
452
|
+
* appends to a merchant's `checkoutSuccessUrl` as `?tradeNo=...`).
|
|
453
|
+
*
|
|
454
|
+
* Returns `null` when the id doesn't resolve on this site — does NOT
|
|
455
|
+
* throw. Use the returned `status` to gate Meta Pixel `Purchase` events:
|
|
456
|
+
*
|
|
457
|
+
* @example
|
|
458
|
+
* const id = new URLSearchParams(location.search).get('tradeNo');
|
|
459
|
+
* if (!id) return;
|
|
460
|
+
* const order = await loopwise.orders.get({ id });
|
|
461
|
+
* if (order?.status === 'PAID') {
|
|
462
|
+
* fbq('track', 'Purchase', {
|
|
463
|
+
* value: order.total,
|
|
464
|
+
* currency: order.currency,
|
|
465
|
+
* contents: order.items.map(i => ({
|
|
466
|
+
* id: i.id, name: i.name, quantity: i.quantity, item_price: i.amount,
|
|
467
|
+
* })),
|
|
468
|
+
* content_ids: order.items.map(i => i.id),
|
|
469
|
+
* content_type: 'product',
|
|
470
|
+
* num_items: order.items.reduce((s, i) => s + i.quantity, 0),
|
|
471
|
+
* }, { eventID: order.id });
|
|
472
|
+
* }
|
|
473
|
+
*/
|
|
474
|
+
get(args, options = {}) {
|
|
475
|
+
const sel = args.select ?? DEFAULT_GET_FIELDS$1;
|
|
476
|
+
return new QueryBuilder(this.client, {
|
|
477
|
+
operation: "OrderGet",
|
|
478
|
+
field: "order",
|
|
479
|
+
selectionSet: buildOrderSelection(sel),
|
|
480
|
+
args: { id: args.id },
|
|
481
|
+
vars: tenantVars$1(this.client),
|
|
482
|
+
transform: (raw) => raw == null ? null : mapOrder(raw)
|
|
483
|
+
}, options);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
//#endregion
|
|
487
|
+
//#region src/schema/posts.ts
|
|
488
|
+
/** Map SDK Post selection -> schema field selection set. */
|
|
489
|
+
function buildPostSelection(select) {
|
|
490
|
+
const parts = ["id", "slug"];
|
|
491
|
+
const sel = select;
|
|
492
|
+
if (sel.title) parts.push("title");
|
|
493
|
+
if (sel.subtitle) parts.push("subtitle");
|
|
494
|
+
if (sel.excerpt) parts.push("excerpt");
|
|
495
|
+
if (sel.body) parts.push("body");
|
|
496
|
+
if (sel.coverImage) parts.push("coverPhoto");
|
|
497
|
+
if (sel.publishedAt) parts.push("publishedAt");
|
|
498
|
+
if (sel.modifiedAt) parts.push("modifiedAt");
|
|
499
|
+
if (sel.accessType) parts.push("accessType");
|
|
500
|
+
if (sel.tags) parts.push("tags");
|
|
501
|
+
if (sel.noindex) parts.push("noindex");
|
|
502
|
+
if (sel.author && typeof sel.author === "object") {
|
|
503
|
+
const auth = sel.author;
|
|
504
|
+
const authParts = ["id"];
|
|
505
|
+
if (auth.name) authParts.push("name");
|
|
506
|
+
if (auth.avatar) authParts.push("avatar");
|
|
507
|
+
parts.push(`author { ${authParts.join(" ")} }`);
|
|
508
|
+
}
|
|
509
|
+
return parts.join(" ");
|
|
510
|
+
}
|
|
511
|
+
function mapPost(raw) {
|
|
512
|
+
const out = {
|
|
513
|
+
id: raw.id,
|
|
514
|
+
slug: raw.slug
|
|
515
|
+
};
|
|
516
|
+
if (raw.title !== void 0) out.title = raw.title;
|
|
517
|
+
if (raw.subtitle !== void 0) out.subtitle = raw.subtitle ?? null;
|
|
518
|
+
if (raw.excerpt !== void 0) out.excerpt = raw.excerpt ?? null;
|
|
519
|
+
if (raw.body !== void 0) out.body = raw.body;
|
|
520
|
+
if (raw.coverPhoto !== void 0) out.coverImage = raw.coverPhoto ?? null;
|
|
521
|
+
if (raw.publishedAt !== void 0) out.publishedAt = raw.publishedAt;
|
|
522
|
+
if (raw.modifiedAt !== void 0) out.modifiedAt = raw.modifiedAt ?? null;
|
|
523
|
+
if (raw.accessType !== void 0) out.accessType = raw.accessType;
|
|
524
|
+
if (raw.tags !== void 0) out.tags = raw.tags ?? null;
|
|
525
|
+
if (raw.noindex !== void 0) out.noindex = raw.noindex;
|
|
526
|
+
if (raw.author !== void 0) out.author = raw.author ? mapLecturer(raw.author) : null;
|
|
527
|
+
return out;
|
|
528
|
+
}
|
|
529
|
+
function mapPostPage(raw) {
|
|
530
|
+
return {
|
|
531
|
+
items: raw.nodes.map(mapPost),
|
|
532
|
+
pageInfo: {
|
|
533
|
+
page: raw.currentPage,
|
|
534
|
+
totalPages: raw.totalPages,
|
|
535
|
+
totalCount: raw.nodesCount,
|
|
536
|
+
hasNext: raw.hasNextPage,
|
|
537
|
+
hasPrev: raw.hasPreviousPage
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
//#endregion
|
|
542
|
+
//#region src/resources/posts.ts
|
|
543
|
+
const DEFAULT_LIST_FIELDS = {
|
|
544
|
+
id: true,
|
|
545
|
+
slug: true,
|
|
546
|
+
title: true,
|
|
547
|
+
excerpt: true,
|
|
548
|
+
coverImage: true,
|
|
549
|
+
publishedAt: true
|
|
550
|
+
};
|
|
551
|
+
const DEFAULT_GET_FIELDS = {
|
|
552
|
+
id: true,
|
|
553
|
+
slug: true,
|
|
554
|
+
title: true,
|
|
555
|
+
subtitle: true,
|
|
556
|
+
excerpt: true,
|
|
557
|
+
body: true,
|
|
558
|
+
coverImage: true,
|
|
559
|
+
publishedAt: true,
|
|
560
|
+
modifiedAt: true,
|
|
561
|
+
accessType: true,
|
|
562
|
+
author: {
|
|
563
|
+
id: true,
|
|
564
|
+
name: true,
|
|
565
|
+
avatar: true
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
function tenantVars(client) {
|
|
569
|
+
return { subdomain: {
|
|
570
|
+
type: "String!",
|
|
571
|
+
value: client.config.site ?? ""
|
|
572
|
+
} };
|
|
573
|
+
}
|
|
574
|
+
var PostsResource = class {
|
|
575
|
+
client;
|
|
576
|
+
constructor(client) {
|
|
577
|
+
this.client = client;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* List published posts, ordered by publishedAt desc.
|
|
581
|
+
* @example
|
|
582
|
+
* const { items, pageInfo } = await loopwise.posts.list({ limit: 10 })
|
|
583
|
+
* @example
|
|
584
|
+
* await loopwise.posts.list({ limit: 10 }, { cache: 60 })
|
|
585
|
+
*/
|
|
586
|
+
list(args = {}, options = {}) {
|
|
587
|
+
const selectionSet = `nodes { ${buildPostSelection(args.select ?? DEFAULT_LIST_FIELDS)} } currentPage totalPages nodesCount hasNextPage hasPreviousPage`;
|
|
588
|
+
return new QueryBuilder(this.client, {
|
|
589
|
+
operation: "PostList",
|
|
590
|
+
field: "posts",
|
|
591
|
+
selectionSet,
|
|
592
|
+
args: {
|
|
593
|
+
page: args.page,
|
|
594
|
+
perPage: args.limit
|
|
595
|
+
},
|
|
596
|
+
vars: tenantVars(this.client),
|
|
597
|
+
transform: (raw) => mapPostPage(raw)
|
|
598
|
+
}, options);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Get a single post by slug.
|
|
602
|
+
*
|
|
603
|
+
* Throws `LoopwiseError("GRAPHQL")` when the slug is not found
|
|
604
|
+
* (Storefront `Queries::Post` raises `service_error("Post-000")` on a
|
|
605
|
+
* `null: false` field). Wrap with try/catch to render 404 routes:
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* try {
|
|
609
|
+
* const post = await loopwise.posts.get({ slug: 'hello-world' })
|
|
610
|
+
* // ...
|
|
611
|
+
* } catch (e) {
|
|
612
|
+
* if (e instanceof LoopwiseError && e.code === 'GRAPHQL') {
|
|
613
|
+
* return Astro.redirect('/404')
|
|
614
|
+
* }
|
|
615
|
+
* throw e
|
|
616
|
+
* }
|
|
617
|
+
*/
|
|
618
|
+
get(args, options = {}) {
|
|
619
|
+
const sel = args.select ?? DEFAULT_GET_FIELDS;
|
|
620
|
+
return new QueryBuilder(this.client, {
|
|
621
|
+
operation: "PostGet",
|
|
622
|
+
field: "post",
|
|
623
|
+
selectionSet: buildPostSelection(sel),
|
|
624
|
+
args: { slug: args.slug },
|
|
369
625
|
vars: tenantVars(this.client),
|
|
370
|
-
transform: (raw) =>
|
|
626
|
+
transform: (raw) => mapPost(raw)
|
|
371
627
|
}, options);
|
|
372
628
|
}
|
|
373
629
|
};
|
|
374
|
-
|
|
375
630
|
//#endregion
|
|
376
631
|
//#region src/index.ts
|
|
377
632
|
function createClient(config) {
|
|
378
633
|
const client = new LoopwiseClient(config);
|
|
379
634
|
return {
|
|
380
635
|
courses: new CoursesResource(client),
|
|
636
|
+
orders: new OrdersResource(client),
|
|
637
|
+
posts: new PostsResource(client),
|
|
381
638
|
batch(builders, options) {
|
|
382
639
|
return client.batch(builders, options);
|
|
383
640
|
},
|
|
@@ -386,6 +643,5 @@ function createClient(config) {
|
|
|
386
643
|
}
|
|
387
644
|
};
|
|
388
645
|
}
|
|
389
|
-
|
|
390
646
|
//#endregion
|
|
391
|
-
export { LoopwiseError, QueryBuilder, createClient };
|
|
647
|
+
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.
|
|
3
|
+
"version": "0.1.0-beta.3",
|
|
4
4
|
"description": "Loopwise platform SDK — agent-friendly headless API client",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,17 +37,20 @@
|
|
|
37
37
|
"files": [
|
|
38
38
|
"dist/*.mjs",
|
|
39
39
|
"dist/*.d.mts",
|
|
40
|
-
"README.md"
|
|
40
|
+
"README.md",
|
|
41
|
+
"CHANGELOG.md"
|
|
41
42
|
],
|
|
42
43
|
"scripts": {
|
|
43
44
|
"build": "tsdown",
|
|
44
45
|
"dev": "tsdown --watch",
|
|
45
46
|
"typecheck": "tsc --noEmit",
|
|
46
|
-
"test": "vitest run"
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"regen:lock": "node scripts/regen-lock.mjs"
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
|
51
|
+
"graphql": "16.13.1",
|
|
49
52
|
"tsconfig": "workspace:*",
|
|
50
|
-
"tsdown": "0.
|
|
53
|
+
"tsdown": "0.22.0",
|
|
51
54
|
"typescript": "5.9.3",
|
|
52
55
|
"vitest": "4.1.0"
|
|
53
56
|
},
|