@misofm/api-client 0.1.2 → 0.3.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/package.json +1 -1
- package/src/client.test.ts +102 -28
- package/src/client.ts +92 -33
- package/src/schemas.ts +78 -45
- package/src/types.ts +1 -1
package/package.json
CHANGED
package/src/client.test.ts
CHANGED
|
@@ -2,17 +2,26 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import { describe, expect, test } from "bun:test";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
cacheBuster,
|
|
7
|
+
createMisoApiClient,
|
|
8
|
+
MisoApiContractError,
|
|
9
|
+
MisoApiError,
|
|
10
|
+
READ_CACHE_CLASS,
|
|
11
|
+
} from "./client.ts";
|
|
6
12
|
|
|
7
13
|
/** A fetch that records the URL it was called with and replays a canned response. */
|
|
8
14
|
function stubFetch(response: { status?: number; body?: unknown }) {
|
|
9
15
|
const calls: string[] = [];
|
|
10
16
|
const fetch = (async (input: string | URL) => {
|
|
11
17
|
calls.push(String(input));
|
|
12
|
-
return new Response(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
return new Response(
|
|
19
|
+
response.body === undefined ? "" : JSON.stringify(response.body),
|
|
20
|
+
{
|
|
21
|
+
status: response.status ?? 200,
|
|
22
|
+
headers: { "Content-Type": "application/json" },
|
|
23
|
+
},
|
|
24
|
+
);
|
|
16
25
|
}) as unknown as typeof globalThis.fetch;
|
|
17
26
|
return { fetch, calls };
|
|
18
27
|
}
|
|
@@ -35,56 +44,98 @@ describe("URL construction", () => {
|
|
|
35
44
|
|
|
36
45
|
test("tolerates a trailing slash on the base URL", async () => {
|
|
37
46
|
const { fetch, calls } = stubFetch({ body: balance });
|
|
38
|
-
await createMisoApiClient({ baseUrl: `${BASE}/`, fetch }).getBalance(
|
|
47
|
+
await createMisoApiClient({ baseUrl: `${BASE}/`, fetch }).getBalance(
|
|
48
|
+
"0xabc",
|
|
49
|
+
);
|
|
39
50
|
expect(calls[0]).toBe(`${BASE}/read/v1/wallets/0xabc/balance`);
|
|
40
51
|
});
|
|
41
52
|
|
|
42
53
|
test("omits empty query params rather than sending them blank", async () => {
|
|
43
54
|
const { fetch, calls } = stubFetch({ body: balance });
|
|
44
|
-
await createMisoApiClient({ baseUrl: BASE, fetch }).getBalance(
|
|
55
|
+
await createMisoApiClient({ baseUrl: BASE, fetch }).getBalance(
|
|
56
|
+
"0xabc",
|
|
57
|
+
undefined,
|
|
58
|
+
);
|
|
45
59
|
expect(calls[0]).not.toContain("coinType");
|
|
46
60
|
});
|
|
47
61
|
|
|
48
62
|
test("sends an explicit coin type when given one", async () => {
|
|
49
63
|
const { fetch, calls } = stubFetch({ body: balance });
|
|
50
|
-
await createMisoApiClient({ baseUrl: BASE, fetch }).getBalance(
|
|
64
|
+
await createMisoApiClient({ baseUrl: BASE, fetch }).getBalance(
|
|
65
|
+
"0xabc",
|
|
66
|
+
"0x2::sui::SUI",
|
|
67
|
+
);
|
|
51
68
|
expect(calls[0]).toContain("coinType=0x2%3A%3Asui%3A%3ASUI");
|
|
52
69
|
});
|
|
53
70
|
|
|
54
71
|
test("joins the artist include list into one param", async () => {
|
|
55
72
|
const { fetch, calls } = stubFetch({ status: 404 });
|
|
56
|
-
await createMisoApiClient({ baseUrl: BASE, fetch }).getArtist("0xp", {
|
|
57
|
-
|
|
73
|
+
await createMisoApiClient({ baseUrl: BASE, fetch }).getArtist("0xp", {
|
|
74
|
+
include: ["roles", "tags", "featured"],
|
|
75
|
+
});
|
|
76
|
+
expect(calls[0]).toContain("include=roles%2Ctags%2Cfeatured");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("joins catalog relationship expansions into one param", async () => {
|
|
80
|
+
const { fetch, calls } = stubFetch({ status: 404 });
|
|
81
|
+
const client = createMisoApiClient({ baseUrl: BASE, fetch });
|
|
82
|
+
await client.getPressing("0xp", { include: ["trackCredits"] });
|
|
83
|
+
await client.getRelease("0xl", { include: ["trackCredits"] });
|
|
84
|
+
await client.getRecordAlbum("0xr", {
|
|
85
|
+
include: ["release", "trackCredits"],
|
|
86
|
+
});
|
|
87
|
+
expect(calls[0]).toContain("include=trackCredits");
|
|
88
|
+
expect(calls[1]).toContain("include=trackCredits");
|
|
89
|
+
expect(calls[2]).toContain("include=release%2CtrackCredits");
|
|
58
90
|
});
|
|
59
91
|
|
|
60
92
|
test("a custom prefix is honored (self-hosted / direct-to-service)", async () => {
|
|
61
93
|
const { fetch, calls } = stubFetch({ body: balance });
|
|
62
|
-
await createMisoApiClient({
|
|
94
|
+
await createMisoApiClient({
|
|
95
|
+
baseUrl: BASE,
|
|
96
|
+
fetch,
|
|
97
|
+
prefix: "/v1",
|
|
98
|
+
}).getBalance("0xabc");
|
|
63
99
|
expect(calls[0]).toBe(`${BASE}/v1/wallets/0xabc/balance`);
|
|
64
100
|
});
|
|
65
101
|
});
|
|
66
102
|
|
|
67
103
|
describe("not-found handling", () => {
|
|
68
104
|
test("a superseded pressing is null, not an error", async () => {
|
|
69
|
-
const { fetch } = stubFetch({
|
|
70
|
-
|
|
105
|
+
const { fetch } = stubFetch({
|
|
106
|
+
status: 404,
|
|
107
|
+
body: { error: { code: "not_found", message: "gone" } },
|
|
108
|
+
});
|
|
109
|
+
expect(
|
|
110
|
+
await createMisoApiClient({ baseUrl: BASE, fetch }).getPressing("0xdead"),
|
|
111
|
+
).toBeNull();
|
|
71
112
|
});
|
|
72
113
|
|
|
73
114
|
test("an unknown artist is null", async () => {
|
|
74
|
-
const { fetch } = stubFetch({
|
|
75
|
-
|
|
115
|
+
const { fetch } = stubFetch({
|
|
116
|
+
status: 404,
|
|
117
|
+
body: { error: { code: "not_found", message: "gone" } },
|
|
118
|
+
});
|
|
119
|
+
expect(
|
|
120
|
+
await createMisoApiClient({ baseUrl: BASE, fetch }).getArtist("0xdead"),
|
|
121
|
+
).toBeNull();
|
|
76
122
|
});
|
|
77
123
|
|
|
78
124
|
test("an empty id list short-circuits without a request", async () => {
|
|
79
125
|
const { fetch, calls } = stubFetch({ body: [] });
|
|
80
|
-
expect(
|
|
126
|
+
expect(
|
|
127
|
+
await createMisoApiClient({ baseUrl: BASE, fetch }).getArtists([]),
|
|
128
|
+
).toEqual([]);
|
|
81
129
|
expect(calls).toHaveLength(0);
|
|
82
130
|
});
|
|
83
131
|
});
|
|
84
132
|
|
|
85
133
|
describe("error handling", () => {
|
|
86
134
|
test("surfaces the server's error code and message", async () => {
|
|
87
|
-
const { fetch } = stubFetch({
|
|
135
|
+
const { fetch } = stubFetch({
|
|
136
|
+
status: 429,
|
|
137
|
+
body: { error: { code: "rate-limited", message: "Too many requests." } },
|
|
138
|
+
});
|
|
88
139
|
const api = createMisoApiClient({ baseUrl: BASE, fetch });
|
|
89
140
|
await expect(api.getDiscover()).rejects.toThrow(MisoApiError);
|
|
90
141
|
try {
|
|
@@ -109,17 +160,24 @@ describe("error handling", () => {
|
|
|
109
160
|
});
|
|
110
161
|
|
|
111
162
|
test("a 404 on an endpoint that must return a body is an error, not null", async () => {
|
|
112
|
-
const { fetch } = stubFetch({
|
|
113
|
-
|
|
163
|
+
const { fetch } = stubFetch({
|
|
164
|
+
status: 404,
|
|
165
|
+
body: { error: { code: "not_found", message: "gone" } },
|
|
166
|
+
});
|
|
167
|
+
await expect(
|
|
168
|
+
createMisoApiClient({ baseUrl: BASE, fetch }).getWalletRecords("0xabc"),
|
|
169
|
+
).rejects.toThrow(MisoApiError);
|
|
114
170
|
});
|
|
115
171
|
});
|
|
116
172
|
|
|
117
173
|
describe("contract validation", () => {
|
|
118
174
|
test("a response missing a required field fails loudly at the boundary", async () => {
|
|
119
|
-
const { fetch } = stubFetch({
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
175
|
+
const { fetch } = stubFetch({
|
|
176
|
+
body: { address: "0xabc", coinType: "0x2::sui::SUI" },
|
|
177
|
+
}); // no balance
|
|
178
|
+
await expect(
|
|
179
|
+
createMisoApiClient({ baseUrl: BASE, fetch }).getBalance("0xabc"),
|
|
180
|
+
).rejects.toThrow(MisoApiContractError);
|
|
123
181
|
});
|
|
124
182
|
|
|
125
183
|
test("a u64 sent as a NUMBER is rejected — the precision bug this contract exists to prevent", async () => {
|
|
@@ -131,7 +189,9 @@ describe("contract validation", () => {
|
|
|
131
189
|
});
|
|
132
190
|
|
|
133
191
|
test("the contract error names the field and points at version skew", async () => {
|
|
134
|
-
const { fetch } = stubFetch({
|
|
192
|
+
const { fetch } = stubFetch({
|
|
193
|
+
body: { ...balance, balance: "not-a-number" },
|
|
194
|
+
});
|
|
135
195
|
try {
|
|
136
196
|
await createMisoApiClient({ baseUrl: BASE, fetch }).getBalance("0xabc");
|
|
137
197
|
throw new Error("should have thrown");
|
|
@@ -143,7 +203,10 @@ describe("contract validation", () => {
|
|
|
143
203
|
|
|
144
204
|
test("a valid response parses through to typed data", async () => {
|
|
145
205
|
const { fetch } = stubFetch({ body: balance });
|
|
146
|
-
const result = await createMisoApiClient({
|
|
206
|
+
const result = await createMisoApiClient({
|
|
207
|
+
baseUrl: BASE,
|
|
208
|
+
fetch,
|
|
209
|
+
}).getBalance("0xabc");
|
|
147
210
|
expect(result).toEqual(balance);
|
|
148
211
|
});
|
|
149
212
|
});
|
|
@@ -151,7 +214,12 @@ describe("contract validation", () => {
|
|
|
151
214
|
describe("READ_CACHE_CLASS", () => {
|
|
152
215
|
test("every wallet-scoped read is private", () => {
|
|
153
216
|
for (const [method, cls] of Object.entries(READ_CACHE_CLASS)) {
|
|
154
|
-
if (
|
|
217
|
+
if (
|
|
218
|
+
method.startsWith("getWallet") ||
|
|
219
|
+
method.startsWith("owns") ||
|
|
220
|
+
method === "getBalance" ||
|
|
221
|
+
method === "getWork"
|
|
222
|
+
) {
|
|
155
223
|
expect(cls).toBe("private");
|
|
156
224
|
}
|
|
157
225
|
}
|
|
@@ -177,7 +245,11 @@ describe("cache buster", () => {
|
|
|
177
245
|
|
|
178
246
|
test("a supplied version rides on every read as ?v=", async () => {
|
|
179
247
|
const { fetch, calls } = stubFetch({ body: balance });
|
|
180
|
-
const api = createMisoApiClient({
|
|
248
|
+
const api = createMisoApiClient({
|
|
249
|
+
baseUrl: BASE,
|
|
250
|
+
fetch,
|
|
251
|
+
version: () => "1699999999",
|
|
252
|
+
});
|
|
181
253
|
await api.getBalance("0xabc");
|
|
182
254
|
expect(calls[0]).toContain("v=1699999999");
|
|
183
255
|
});
|
|
@@ -196,6 +268,8 @@ describe("cache buster", () => {
|
|
|
196
268
|
test("cacheBuster is per-SECOND, so a burst after one write shares an entry", () => {
|
|
197
269
|
// Millisecond granularity would mint a fresh cache entry per read.
|
|
198
270
|
expect(cacheBuster(1_700_000_000_123)).toBe(cacheBuster(1_700_000_000_900));
|
|
199
|
-
expect(cacheBuster(1_700_000_000_000)).not.toBe(
|
|
271
|
+
expect(cacheBuster(1_700_000_000_000)).not.toBe(
|
|
272
|
+
cacheBuster(1_700_000_001_000),
|
|
273
|
+
);
|
|
200
274
|
});
|
|
201
275
|
});
|
package/src/client.ts
CHANGED
|
@@ -23,8 +23,8 @@ import type {
|
|
|
23
23
|
Balance,
|
|
24
24
|
DiscoverShelf,
|
|
25
25
|
DropPreview,
|
|
26
|
-
FeaturedRelease,
|
|
27
26
|
OwnedParty,
|
|
27
|
+
PendingMembership,
|
|
28
28
|
OwnedRecord,
|
|
29
29
|
OwnedWork,
|
|
30
30
|
Ownership,
|
|
@@ -33,7 +33,6 @@ import type {
|
|
|
33
33
|
PurchaseReceipt,
|
|
34
34
|
RecordAlbum,
|
|
35
35
|
ReleaseDetail,
|
|
36
|
-
ReleaseTrackCredits,
|
|
37
36
|
WorkDetail,
|
|
38
37
|
} from "./types.ts";
|
|
39
38
|
|
|
@@ -102,7 +101,8 @@ export function createMisoApiClient(options: MisoApiClientOptions) {
|
|
|
102
101
|
function url(path: string, query: Record<string, QueryValue> = {}): string {
|
|
103
102
|
const u = new URL(`${base}${prefix}${path}`);
|
|
104
103
|
for (const [k, v] of Object.entries(query)) {
|
|
105
|
-
if (v !== undefined && v !== null && v !== "")
|
|
104
|
+
if (v !== undefined && v !== null && v !== "")
|
|
105
|
+
u.searchParams.set(k, String(v));
|
|
106
106
|
}
|
|
107
107
|
const v = options.version?.();
|
|
108
108
|
if (v) u.searchParams.set("v", v);
|
|
@@ -116,7 +116,9 @@ export function createMisoApiClient(options: MisoApiClientOptions) {
|
|
|
116
116
|
opts: { nullOn404?: boolean } = {},
|
|
117
117
|
): Promise<T | null> {
|
|
118
118
|
const target = url(path, query);
|
|
119
|
-
const res = await doFetch(target, {
|
|
119
|
+
const res = await doFetch(target, {
|
|
120
|
+
headers: { Accept: "application/json" },
|
|
121
|
+
});
|
|
120
122
|
|
|
121
123
|
if (res.status === 404 && opts.nullOn404) return null;
|
|
122
124
|
|
|
@@ -124,52 +126,98 @@ export function createMisoApiClient(options: MisoApiClientOptions) {
|
|
|
124
126
|
const body = await res.json().catch(() => null);
|
|
125
127
|
const parsed = s.apiErrorSchema.safeParse(body);
|
|
126
128
|
throw parsed.success
|
|
127
|
-
? new MisoApiError(
|
|
128
|
-
|
|
129
|
+
? new MisoApiError(
|
|
130
|
+
res.status,
|
|
131
|
+
parsed.data.error.code,
|
|
132
|
+
parsed.data.error.message,
|
|
133
|
+
)
|
|
134
|
+
: new MisoApiError(
|
|
135
|
+
res.status,
|
|
136
|
+
"unknown",
|
|
137
|
+
`Request to ${path} failed (${res.status})`,
|
|
138
|
+
);
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
const json = await res.json().catch(() => {
|
|
132
|
-
throw new MisoApiError(
|
|
142
|
+
throw new MisoApiError(
|
|
143
|
+
res.status,
|
|
144
|
+
"bad_response",
|
|
145
|
+
`Response from ${path} was not JSON`,
|
|
146
|
+
);
|
|
133
147
|
});
|
|
134
148
|
|
|
135
149
|
const parsed = schema.safeParse(json);
|
|
136
|
-
if (!parsed.success)
|
|
150
|
+
if (!parsed.success)
|
|
151
|
+
throw new MisoApiContractError(path, parsed.error.issues);
|
|
137
152
|
return parsed.data;
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
/** Same as `request`, for endpoints that always return a body. */
|
|
141
|
-
async function required<T>(
|
|
156
|
+
async function required<T>(
|
|
157
|
+
schema: z.ZodType<T>,
|
|
158
|
+
path: string,
|
|
159
|
+
query?: Record<string, QueryValue>,
|
|
160
|
+
): Promise<T> {
|
|
142
161
|
return (await request(schema, path, query)) as T;
|
|
143
162
|
}
|
|
144
163
|
|
|
145
164
|
return {
|
|
146
165
|
// ── Catalog ─────────────────────────────────────────────────────────────
|
|
147
166
|
/** The records currently on sale. */
|
|
148
|
-
getDiscover: (): Promise<DiscoverShelf> =>
|
|
167
|
+
getDiscover: (): Promise<DiscoverShelf> =>
|
|
168
|
+
required(s.discoverShelfSchema, "/discover"),
|
|
149
169
|
|
|
150
170
|
/** A pressing and everything its buy page renders. `null` when there is no such pressing. */
|
|
151
|
-
getPressing: (
|
|
152
|
-
|
|
171
|
+
getPressing: (
|
|
172
|
+
pressingId: string,
|
|
173
|
+
opts: { include?: readonly "trackCredits"[] } = {},
|
|
174
|
+
): Promise<PressingDetail | null> =>
|
|
175
|
+
request(
|
|
176
|
+
s.pressingDetailSchema,
|
|
177
|
+
`/pressings/${pressingId}`,
|
|
178
|
+
{ include: opts.include?.join(",") },
|
|
179
|
+
{ nullOn404: true },
|
|
180
|
+
),
|
|
153
181
|
|
|
154
182
|
/** A release with its cover, credits, and resolved tracklist. */
|
|
155
|
-
getRelease: (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
183
|
+
getRelease: (
|
|
184
|
+
releaseId: string,
|
|
185
|
+
opts: { include?: readonly "trackCredits"[] } = {},
|
|
186
|
+
): Promise<ReleaseDetail | null> =>
|
|
187
|
+
request(
|
|
188
|
+
s.releaseDetailSchema,
|
|
189
|
+
`/releases/${releaseId}`,
|
|
190
|
+
{ include: opts.include?.join(",") },
|
|
191
|
+
{ nullOn404: true },
|
|
192
|
+
),
|
|
161
193
|
|
|
162
194
|
/** A record's parent release. Immutable for the life of the record. */
|
|
163
|
-
getRecordAlbum: (
|
|
164
|
-
|
|
195
|
+
getRecordAlbum: (
|
|
196
|
+
recordId: string,
|
|
197
|
+
opts: { include?: readonly ("release" | "trackCredits")[] } = {},
|
|
198
|
+
): Promise<RecordAlbum | null> =>
|
|
199
|
+
request(
|
|
200
|
+
s.recordAlbumSchema,
|
|
201
|
+
`/records/${recordId}/album`,
|
|
202
|
+
{ include: opts.include?.join(",") },
|
|
203
|
+
{ nullOn404: true },
|
|
204
|
+
),
|
|
165
205
|
|
|
166
206
|
/** Confirmation preview for a pasted pressing id. `null` when it isn't a pressing. */
|
|
167
207
|
getDropPreview: (dropId: string): Promise<DropPreview | null> =>
|
|
168
|
-
request(
|
|
208
|
+
request(
|
|
209
|
+
s.dropPreviewSchema,
|
|
210
|
+
`/drops/${dropId}/preview`,
|
|
211
|
+
{},
|
|
212
|
+
{ nullOn404: true },
|
|
213
|
+
),
|
|
169
214
|
|
|
170
215
|
// ── Artist ──────────────────────────────────────────────────────────────
|
|
171
|
-
/** An artist
|
|
172
|
-
getArtist: (
|
|
216
|
+
/** An artist plus optional relationship expansions. */
|
|
217
|
+
getArtist: (
|
|
218
|
+
partyId: string,
|
|
219
|
+
opts: { include?: readonly ("roles" | "tags" | "featured")[] } = {},
|
|
220
|
+
): Promise<ArtistProfile | null> =>
|
|
173
221
|
request(
|
|
174
222
|
s.artistProfileSchema,
|
|
175
223
|
`/artists/${partyId}`,
|
|
@@ -177,10 +225,6 @@ export function createMisoApiClient(options: MisoApiClientOptions) {
|
|
|
177
225
|
{ nullOn404: true },
|
|
178
226
|
),
|
|
179
227
|
|
|
180
|
-
/** The party's pinned pressing, resolved. `null` when nothing is pinned. */
|
|
181
|
-
getArtistFeatured: (partyId: string): Promise<FeaturedRelease | null> =>
|
|
182
|
-
request(s.featuredReleaseSchema.nullable(), `/artists/${partyId}/featured`) as Promise<FeaturedRelease | null>,
|
|
183
|
-
|
|
184
228
|
/** Name + kind for many parties at once. */
|
|
185
229
|
getArtists: (ids: readonly string[]): Promise<PartySummary[]> =>
|
|
186
230
|
ids.length === 0
|
|
@@ -196,6 +240,10 @@ export function createMisoApiClient(options: MisoApiClientOptions) {
|
|
|
196
240
|
getWalletParties: (address: string): Promise<OwnedParty[]> =>
|
|
197
241
|
required(s.ownedPartiesSchema, `/wallets/${address}/parties`),
|
|
198
242
|
|
|
243
|
+
/** Group invitations awaiting acceptance by this wallet's controlled parties. */
|
|
244
|
+
getPendingMemberships: (address: string): Promise<PendingMembership[]> =>
|
|
245
|
+
required(s.pendingMembershipsSchema, `/wallets/${address}/pending-memberships`),
|
|
246
|
+
|
|
199
247
|
/** The works this wallet administers, keyed by admin cap. */
|
|
200
248
|
getWalletWorks: (address: string): Promise<OwnedWork[]> =>
|
|
201
249
|
required(s.ownedWorksSchema, `/wallets/${address}/works`),
|
|
@@ -210,16 +258,28 @@ export function createMisoApiClient(options: MisoApiClientOptions) {
|
|
|
210
258
|
|
|
211
259
|
/** Whether the wallet holds this party's admin cap. Carries the cap id. */
|
|
212
260
|
ownsParty: (address: string, partyId: string): Promise<Ownership> =>
|
|
213
|
-
required(s.ownershipSchema, `/wallets/${address}/owns`, {
|
|
261
|
+
required(s.ownershipSchema, `/wallets/${address}/owns`, {
|
|
262
|
+
party: partyId,
|
|
263
|
+
}),
|
|
214
264
|
|
|
215
265
|
/** Whether the wallet owns this record. */
|
|
216
266
|
ownsRecord: (address: string, recordId: string): Promise<Ownership> =>
|
|
217
|
-
required(s.ownershipSchema, `/wallets/${address}/owns`, {
|
|
267
|
+
required(s.ownershipSchema, `/wallets/${address}/owns`, {
|
|
268
|
+
record: recordId,
|
|
269
|
+
}),
|
|
218
270
|
|
|
219
271
|
// ── Receipts ────────────────────────────────────────────────────────────
|
|
220
272
|
/** What one record sale was, re-derived from its transaction. */
|
|
221
|
-
getReceipt: (
|
|
222
|
-
|
|
273
|
+
getReceipt: (
|
|
274
|
+
pressingId: string,
|
|
275
|
+
txDigest: string,
|
|
276
|
+
): Promise<PurchaseReceipt | null> =>
|
|
277
|
+
request(
|
|
278
|
+
s.purchaseReceiptSchema,
|
|
279
|
+
`/receipts/${pressingId}/${txDigest}`,
|
|
280
|
+
{},
|
|
281
|
+
{ nullOn404: true },
|
|
282
|
+
),
|
|
223
283
|
};
|
|
224
284
|
}
|
|
225
285
|
|
|
@@ -254,14 +314,13 @@ export const READ_CACHE_CLASS = {
|
|
|
254
314
|
getDiscover: "sale",
|
|
255
315
|
getPressing: "sale",
|
|
256
316
|
getDropPreview: "sale",
|
|
257
|
-
getReleaseTrackCredits: "published",
|
|
258
317
|
getRecordAlbum: "immutable",
|
|
259
318
|
getReceipt: "immutable",
|
|
260
319
|
getArtist: "artist",
|
|
261
|
-
getArtistFeatured: "artist",
|
|
262
320
|
getArtists: "artist",
|
|
263
321
|
getWalletRecords: "private",
|
|
264
322
|
getWalletParties: "private",
|
|
323
|
+
getPendingMemberships: "private",
|
|
265
324
|
getWalletWorks: "private",
|
|
266
325
|
getWork: "private",
|
|
267
326
|
getBalance: "private",
|
package/src/schemas.ts
CHANGED
|
@@ -63,9 +63,36 @@ export const trackViewSchema = z.object({
|
|
|
63
63
|
disc: z.number().int().min(1),
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
/** A recording's work-role credits and recording billing positions. */
|
|
67
|
+
export const recordingCreditsSchema = z.object({
|
|
68
|
+
credits: z.array(creditSchema),
|
|
69
|
+
primaryArtistIds: z.array(suiIdSchema),
|
|
70
|
+
featuredArtistIds: z.array(suiIdSchema),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/** Per-track credits for a release. Composition writing and recording
|
|
74
|
+
* performance/production credits remain separate. */
|
|
75
|
+
const completeTrackCreditsSchema = z.object({
|
|
76
|
+
compositionCredits: z.array(creditSchema),
|
|
77
|
+
recordingCredits: recordingCreditsSchema,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** The HTTP wire accepts the complete shape and the deployed recording-only shape. */
|
|
81
|
+
export const trackCreditsWireSchema = z.union([
|
|
82
|
+
completeTrackCreditsSchema,
|
|
83
|
+
recordingCreditsSchema,
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
/** Clients always receive the complete shape, including during the rollout. */
|
|
87
|
+
export const trackCreditsSchema = trackCreditsWireSchema.transform((credits) =>
|
|
88
|
+
"recordingCredits" in credits
|
|
89
|
+
? credits
|
|
90
|
+
: { compositionCredits: [], recordingCredits: credits },
|
|
91
|
+
);
|
|
92
|
+
|
|
66
93
|
// ── Catalog ──────────────────────────────────────────────────────────────────
|
|
67
94
|
|
|
68
|
-
|
|
95
|
+
const releaseDetailBaseSchema = z.object({
|
|
69
96
|
id: suiIdSchema,
|
|
70
97
|
title: z.string(),
|
|
71
98
|
subtitle: z.string().nullable(),
|
|
@@ -79,6 +106,16 @@ export const releaseDetailSchema = z.object({
|
|
|
79
106
|
tracks: z.array(trackViewSchema),
|
|
80
107
|
});
|
|
81
108
|
|
|
109
|
+
/** Transform-free wire form used by OpenAPI generation. */
|
|
110
|
+
export const releaseDetailWireSchema = releaseDetailBaseSchema.extend({
|
|
111
|
+
trackCredits: z.record(suiIdSchema, trackCreditsWireSchema).optional(),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export const releaseDetailSchema = releaseDetailBaseSchema.extend({
|
|
115
|
+
/** Present only when requested via `include`. */
|
|
116
|
+
trackCredits: z.record(suiIdSchema, trackCreditsSchema).optional(),
|
|
117
|
+
});
|
|
118
|
+
|
|
82
119
|
export const priceSchema = z.object({
|
|
83
120
|
/** `fixed` — pay exactly this. `floor` — pay at least this. */
|
|
84
121
|
kind: z.enum(["fixed", "floor"]),
|
|
@@ -111,6 +148,11 @@ export const pressingDetailSchema = z.object({
|
|
|
111
148
|
release: releaseDetailSchema,
|
|
112
149
|
});
|
|
113
150
|
|
|
151
|
+
export const pressingDetailWireSchema = z.object({
|
|
152
|
+
pressing: pressingViewSchema,
|
|
153
|
+
release: releaseDetailWireSchema,
|
|
154
|
+
});
|
|
155
|
+
|
|
114
156
|
export const discoverItemSchema = z.object({
|
|
115
157
|
pressing: pressingViewSchema,
|
|
116
158
|
releaseId: suiIdSchema,
|
|
@@ -125,6 +167,14 @@ export const discoverShelfSchema = z.array(discoverItemSchema);
|
|
|
125
167
|
export const recordAlbumSchema = z.object({
|
|
126
168
|
recordId: suiIdSchema,
|
|
127
169
|
releaseId: suiIdSchema.nullable(),
|
|
170
|
+
/** Present only when requested via `include`. */
|
|
171
|
+
release: releaseDetailSchema.nullable().optional(),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export const recordAlbumWireSchema = z.object({
|
|
175
|
+
recordId: suiIdSchema,
|
|
176
|
+
releaseId: suiIdSchema.nullable(),
|
|
177
|
+
release: releaseDetailWireSchema.nullable().optional(),
|
|
128
178
|
});
|
|
129
179
|
|
|
130
180
|
export const dropPreviewSchema = z.object({
|
|
@@ -137,42 +187,6 @@ export const dropPreviewSchema = z.object({
|
|
|
137
187
|
trackCount: z.number().int().min(0),
|
|
138
188
|
});
|
|
139
189
|
|
|
140
|
-
/** A recording's work-role credits and recording billing positions. */
|
|
141
|
-
export const recordingCreditsSchema = z.object({
|
|
142
|
-
credits: z.array(creditSchema),
|
|
143
|
-
primaryArtistIds: z.array(suiIdSchema),
|
|
144
|
-
featuredArtistIds: z.array(suiIdSchema),
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
/** Per-track credits for a release. Composition writing and recording
|
|
148
|
-
* performance/production credits remain separate. */
|
|
149
|
-
const completeTrackCreditsSchema = z.object({
|
|
150
|
-
compositionCredits: z.array(creditSchema),
|
|
151
|
-
recordingCredits: recordingCreditsSchema,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
/** The HTTP wire accepts the complete shape and the deployed recording-only shape. */
|
|
155
|
-
export const trackCreditsWireSchema = z.union([
|
|
156
|
-
completeTrackCreditsSchema,
|
|
157
|
-
recordingCreditsSchema,
|
|
158
|
-
]);
|
|
159
|
-
|
|
160
|
-
/** Clients always receive the complete shape, including during the rollout. */
|
|
161
|
-
export const trackCreditsSchema = trackCreditsWireSchema.transform((credits) =>
|
|
162
|
-
"recordingCredits" in credits
|
|
163
|
-
? credits
|
|
164
|
-
: { compositionCredits: [], recordingCredits: credits },
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
/** Transform-free wire schema used when generating OpenAPI. */
|
|
168
|
-
export const releaseTrackCreditsWireSchema = z.object({
|
|
169
|
-
tracks: z.record(suiIdSchema, trackCreditsWireSchema),
|
|
170
|
-
});
|
|
171
|
-
/** Per-track work credits for a release, keyed by recording id. */
|
|
172
|
-
export const releaseTrackCreditsSchema = z.object({
|
|
173
|
-
tracks: z.record(suiIdSchema, trackCreditsSchema),
|
|
174
|
-
});
|
|
175
|
-
|
|
176
190
|
// ── Artist ───────────────────────────────────────────────────────────────────
|
|
177
191
|
|
|
178
192
|
export const partyMemberSchema = z.object({
|
|
@@ -229,6 +243,14 @@ export const partyLinkSchema = z.object({
|
|
|
229
243
|
|
|
230
244
|
export const partyCtaSchema = z.object({ label: z.string(), url: z.string() });
|
|
231
245
|
|
|
246
|
+
export const featuredReleaseSchema = z.object({
|
|
247
|
+
pressingId: suiIdSchema,
|
|
248
|
+
releaseId: suiIdSchema,
|
|
249
|
+
title: z.string(),
|
|
250
|
+
artist: z.string(),
|
|
251
|
+
coverUrl: z.string().url().nullable(),
|
|
252
|
+
});
|
|
253
|
+
|
|
232
254
|
export const artistProfileSchema = z.object({
|
|
233
255
|
id: suiIdSchema,
|
|
234
256
|
kind: z.enum(["individual", "group"]),
|
|
@@ -246,17 +268,11 @@ export const artistProfileSchema = z.object({
|
|
|
246
268
|
/** Present only when requested via `include` — the owner-editor fields. */
|
|
247
269
|
roles: z.array(z.string()).optional(),
|
|
248
270
|
tags: z.array(z.string()).optional(),
|
|
271
|
+
/** Present only when requested; null means the artist has no pinned release. */
|
|
272
|
+
featured: featuredReleaseSchema.nullable().optional(),
|
|
249
273
|
avatarUrl: z.string().url(),
|
|
250
274
|
});
|
|
251
275
|
|
|
252
|
-
export const featuredReleaseSchema = z.object({
|
|
253
|
-
pressingId: suiIdSchema,
|
|
254
|
-
releaseId: suiIdSchema,
|
|
255
|
-
title: z.string(),
|
|
256
|
-
artist: z.string(),
|
|
257
|
-
coverUrl: z.string().url().nullable(),
|
|
258
|
-
});
|
|
259
|
-
|
|
260
276
|
export const partySummarySchema = z.object({
|
|
261
277
|
id: suiIdSchema,
|
|
262
278
|
name: z.string(),
|
|
@@ -286,6 +302,16 @@ export const ownedPartySchema = z.object({
|
|
|
286
302
|
|
|
287
303
|
export const ownedPartiesSchema = z.array(ownedPartySchema);
|
|
288
304
|
|
|
305
|
+
/** A group invitation awaiting action by a party the wallet administers. */
|
|
306
|
+
export const pendingMembershipSchema = z.object({
|
|
307
|
+
memberPartyId: suiIdSchema,
|
|
308
|
+
memberCapId: suiIdSchema,
|
|
309
|
+
groupId: suiIdSchema,
|
|
310
|
+
groupName: z.string(),
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
export const pendingMembershipsSchema = z.array(pendingMembershipSchema);
|
|
314
|
+
|
|
289
315
|
export const workKindSchema = z.enum(["composition", "recording", "release"]);
|
|
290
316
|
|
|
291
317
|
export const ownedWorkSchema = z.object({
|
|
@@ -354,6 +380,13 @@ export const purchaseReceiptSchema = z.object({
|
|
|
354
380
|
tracks: z.array(trackRoyaltySchema),
|
|
355
381
|
});
|
|
356
382
|
|
|
383
|
+
export const purchaseReceiptWireSchema = z.object({
|
|
384
|
+
sale: recordSaleSchema,
|
|
385
|
+
detail: pressingDetailWireSchema,
|
|
386
|
+
price: u64Schema,
|
|
387
|
+
tracks: z.array(trackRoyaltySchema),
|
|
388
|
+
});
|
|
389
|
+
|
|
357
390
|
// ── Errors ───────────────────────────────────────────────────────────────────
|
|
358
391
|
|
|
359
392
|
/** The envelope every non-2xx carries, matching miso-api's `apiError`. */
|
package/src/types.ts
CHANGED
|
@@ -15,7 +15,6 @@ export type TrackView = z.infer<typeof s.trackViewSchema>;
|
|
|
15
15
|
|
|
16
16
|
export type ReleaseDetail = z.infer<typeof s.releaseDetailSchema>;
|
|
17
17
|
export type TrackCredits = z.infer<typeof s.trackCreditsSchema>;
|
|
18
|
-
export type ReleaseTrackCredits = z.infer<typeof s.releaseTrackCreditsSchema>;
|
|
19
18
|
export type Price = z.infer<typeof s.priceSchema>;
|
|
20
19
|
export type Currency = z.infer<typeof s.currencySchema>;
|
|
21
20
|
export type PressingView = z.infer<typeof s.pressingViewSchema>;
|
|
@@ -35,6 +34,7 @@ export type PartySummary = z.infer<typeof s.partySummarySchema>;
|
|
|
35
34
|
|
|
36
35
|
export type OwnedRecord = z.infer<typeof s.ownedRecordSchema>;
|
|
37
36
|
export type OwnedParty = z.infer<typeof s.ownedPartySchema>;
|
|
37
|
+
export type PendingMembership = z.infer<typeof s.pendingMembershipSchema>;
|
|
38
38
|
export type WorkKind = z.infer<typeof s.workKindSchema>;
|
|
39
39
|
export type OwnedWork = z.infer<typeof s.ownedWorkSchema>;
|
|
40
40
|
export type WorkDetail = z.infer<typeof s.workDetailSchema>;
|