@labelgrid/core 0.2.1 → 0.2.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 +26 -0
- package/dist/api/http.d.ts +9 -1
- package/dist/api/http.js +33 -9
- package/dist/entities.d.ts +14 -0
- package/dist/entities.js +14 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
`@labelgrid/core` is primarily an internal shared client for the LabelGrid MCP
|
|
9
9
|
server and CLI — there are no API-stability promises before 1.0.
|
|
10
10
|
|
|
11
|
+
## [Unreleased]
|
|
12
|
+
|
|
13
|
+
## [0.2.3] - 2026-09-30
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Public API `error_code` and `details` fields are preserved on normalized
|
|
18
|
+
errors, including typed 404 and 5xx responses.
|
|
19
|
+
- Normalized errors also preserve the API's `blocking_issues` field, which
|
|
20
|
+
lists the review issues that block a confirm-review request.
|
|
21
|
+
|
|
22
|
+
## [0.2.2] - 2026-08-20
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Catalog entities now declare whether their delete endpoint accepts a
|
|
27
|
+
replacement, allowing clients to expose reassignment only for writers and
|
|
28
|
+
publishers.
|
|
29
|
+
- `LabelGridClient.delete()` accepts optional query parameters while preserving
|
|
30
|
+
the existing request URL byte-for-byte when none are supplied.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Writer and publisher delete guidance now names every reference that can block
|
|
35
|
+
deletion.
|
|
36
|
+
|
|
11
37
|
## [0.2.1] - 2026-08-05
|
|
12
38
|
|
|
13
39
|
### Changed
|
package/dist/api/http.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ export type ApiError = {
|
|
|
15
15
|
suggestion?: string;
|
|
16
16
|
retry_after_seconds?: number;
|
|
17
17
|
errors?: unknown;
|
|
18
|
+
details?: unknown;
|
|
19
|
+
/** Actionable release-review issues returned when confirm-review is blocked. */
|
|
20
|
+
blocking_issues?: unknown;
|
|
18
21
|
/** Structured validation detail passed through verbatim from the API (422). */
|
|
19
22
|
errors_structured?: unknown;
|
|
20
23
|
};
|
|
@@ -72,7 +75,12 @@ export declare class LabelGridClient {
|
|
|
72
75
|
idempotency?: boolean;
|
|
73
76
|
idempotencyKey?: string;
|
|
74
77
|
}): Promise<ApiResult<T>>;
|
|
75
|
-
|
|
78
|
+
/**
|
|
79
|
+
* A delete carries its parameters in the query string, never in a body: RFC
|
|
80
|
+
* 9110 gives a DELETE body no defined semantics and intermediaries drop it.
|
|
81
|
+
* Omitting `query` sends exactly the URL it always did.
|
|
82
|
+
*/
|
|
83
|
+
delete<T>(path: string, query?: Record<string, unknown>): Promise<ApiResult<T>>;
|
|
76
84
|
/**
|
|
77
85
|
* Sends a multipart/form-data POST with a single file field plus optional
|
|
78
86
|
* extra string fields. A missing/unreadable file yields a FILE_NOT_FOUND
|
package/dist/api/http.js
CHANGED
|
@@ -56,8 +56,8 @@ function buildQuery(query) {
|
|
|
56
56
|
return parts.length > 0 ? `?${parts.join('&')}` : '';
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* Extracts
|
|
60
|
-
*
|
|
59
|
+
* Extracts typed fields from the backend's top-level and nested error shapes,
|
|
60
|
+
* including public `error_code` and `details` fields.
|
|
61
61
|
*/
|
|
62
62
|
function extractServerError(body) {
|
|
63
63
|
if (typeof body === 'string') {
|
|
@@ -69,11 +69,22 @@ function extractServerError(body) {
|
|
|
69
69
|
const record = body;
|
|
70
70
|
const errors = record.errors;
|
|
71
71
|
const errorsStructured = record.errors_structured;
|
|
72
|
+
const details = record.details;
|
|
73
|
+
const blockingIssues = record.blocking_issues;
|
|
74
|
+
const topLevelCode = typeof record.error_code === 'string'
|
|
75
|
+
? record.error_code
|
|
76
|
+
: typeof record.code === 'string'
|
|
77
|
+
? record.code
|
|
78
|
+
: undefined;
|
|
72
79
|
// Shape: { error: { code, message } }
|
|
73
80
|
if (record.error !== null && typeof record.error === 'object') {
|
|
74
81
|
const nested = record.error;
|
|
75
82
|
return {
|
|
76
|
-
code: typeof nested.
|
|
83
|
+
code: typeof nested.error_code === 'string'
|
|
84
|
+
? nested.error_code
|
|
85
|
+
: typeof nested.code === 'string'
|
|
86
|
+
? nested.code
|
|
87
|
+
: topLevelCode,
|
|
77
88
|
message: typeof nested.message === 'string'
|
|
78
89
|
? nested.message
|
|
79
90
|
: typeof nested.error === 'string'
|
|
@@ -81,24 +92,30 @@ function extractServerError(body) {
|
|
|
81
92
|
: undefined,
|
|
82
93
|
errors,
|
|
83
94
|
errors_structured: errorsStructured,
|
|
95
|
+
details,
|
|
96
|
+
blocking_issues: blockingIssues,
|
|
84
97
|
};
|
|
85
98
|
}
|
|
86
99
|
// Shape: { error: 'string' }
|
|
87
100
|
if (typeof record.error === 'string') {
|
|
88
101
|
return {
|
|
89
|
-
code:
|
|
102
|
+
code: topLevelCode,
|
|
90
103
|
message: record.error,
|
|
91
104
|
errors,
|
|
92
105
|
errors_structured: errorsStructured,
|
|
106
|
+
details,
|
|
107
|
+
blocking_issues: blockingIssues,
|
|
93
108
|
};
|
|
94
109
|
}
|
|
95
110
|
// Shapes: { message } and/or { errors } and/or top-level { code }
|
|
96
111
|
const parts = {
|
|
97
|
-
code:
|
|
112
|
+
code: topLevelCode,
|
|
98
113
|
message: typeof record.message === 'string' ? record.message : undefined,
|
|
99
114
|
field: typeof record.field === 'string' ? record.field : undefined,
|
|
100
115
|
errors,
|
|
101
116
|
errors_structured: errorsStructured,
|
|
117
|
+
details,
|
|
118
|
+
blocking_issues: blockingIssues,
|
|
102
119
|
};
|
|
103
120
|
// Derive a message from the first validation error when none was given.
|
|
104
121
|
if (parts.message === undefined && errors !== null && typeof errors === 'object') {
|
|
@@ -129,6 +146,8 @@ function normalizeError(res, body) {
|
|
|
129
146
|
status,
|
|
130
147
|
...(server.field !== undefined ? { field: server.field } : {}),
|
|
131
148
|
...(server.errors !== undefined ? { errors: server.errors } : {}),
|
|
149
|
+
...(server.details !== undefined ? { details: server.details } : {}),
|
|
150
|
+
...(server.blocking_issues !== undefined ? { blocking_issues: server.blocking_issues } : {}),
|
|
132
151
|
...extra,
|
|
133
152
|
});
|
|
134
153
|
switch (status) {
|
|
@@ -139,7 +158,7 @@ function normalizeError(res, body) {
|
|
|
139
158
|
case 403:
|
|
140
159
|
return withCommon(server.code ?? 'FORBIDDEN', server.message ?? 'Forbidden.');
|
|
141
160
|
case 404:
|
|
142
|
-
return withCommon('NOT_FOUND', server.message ?? 'The requested resource was not found.');
|
|
161
|
+
return withCommon(server.code ?? 'NOT_FOUND', server.message ?? 'The requested resource was not found.');
|
|
143
162
|
case 409:
|
|
144
163
|
return withCommon(server.code ?? 'CONFLICT', server.message ?? 'The request conflicts with the current state.');
|
|
145
164
|
case 422:
|
|
@@ -156,7 +175,7 @@ function normalizeError(res, body) {
|
|
|
156
175
|
}
|
|
157
176
|
default:
|
|
158
177
|
if (status >= 500) {
|
|
159
|
-
return withCommon('SERVER_ERROR', server.message ?? 'The server encountered an error.');
|
|
178
|
+
return withCommon(server.code ?? 'SERVER_ERROR', server.message ?? 'The server encountered an error.');
|
|
160
179
|
}
|
|
161
180
|
return withCommon(server.code ?? 'ERROR', server.message ?? `Request failed with status ${status}.`);
|
|
162
181
|
}
|
|
@@ -357,8 +376,13 @@ export class LabelGridClient {
|
|
|
357
376
|
idempotencyKey: opts?.idempotencyKey,
|
|
358
377
|
});
|
|
359
378
|
}
|
|
360
|
-
|
|
361
|
-
|
|
379
|
+
/**
|
|
380
|
+
* A delete carries its parameters in the query string, never in a body: RFC
|
|
381
|
+
* 9110 gives a DELETE body no defined semantics and intermediaries drop it.
|
|
382
|
+
* Omitting `query` sends exactly the URL it always did.
|
|
383
|
+
*/
|
|
384
|
+
delete(path, query) {
|
|
385
|
+
return this.send('DELETE', path, { query });
|
|
362
386
|
}
|
|
363
387
|
/**
|
|
364
388
|
* Sends a multipart/form-data POST with a single file field plus optional
|
package/dist/entities.d.ts
CHANGED
|
@@ -23,5 +23,19 @@ export type EntitySpec = {
|
|
|
23
23
|
fieldsDoc: string;
|
|
24
24
|
/** One-line doc of the server-side delete refusals. */
|
|
25
25
|
deleteNote: string;
|
|
26
|
+
/**
|
|
27
|
+
* Whether this entity's DELETE endpoint accepts `replace_with` — the id of the
|
|
28
|
+
* entity that takes over every credit held by the one being deleted, rewritten
|
|
29
|
+
* before the delete. Only the writer and publisher endpoints have it. Stated
|
|
30
|
+
* for every entity rather than defaulted, so a new entity cannot inherit an
|
|
31
|
+
* answer nobody gave.
|
|
32
|
+
*/
|
|
33
|
+
acceptsDeleteReplacement: boolean;
|
|
26
34
|
};
|
|
27
35
|
export declare const ENTITIES: Record<EntityName, EntitySpec>;
|
|
36
|
+
/**
|
|
37
|
+
* The entities whose DELETE accepts `replace_with`, derived from the registry so
|
|
38
|
+
* the tool schema, its description and its refusal message cannot disagree with
|
|
39
|
+
* the table or with each other.
|
|
40
|
+
*/
|
|
41
|
+
export declare const REPLACEMENT_ENTITIES: readonly EntityName[];
|
package/dist/entities.js
CHANGED
|
@@ -19,35 +19,47 @@ export const ENTITIES = {
|
|
|
19
19
|
filtersDoc: 'label: no documented filters.',
|
|
20
20
|
fieldsDoc: 'label — required: name, default_email; optional: support email, website/platform URLs, default copyright lines, isrc_base.',
|
|
21
21
|
deleteNote: 'label: refused while the label still has releases — remove or reassign its releases first.',
|
|
22
|
+
acceptsDeleteReplacement: false,
|
|
22
23
|
},
|
|
23
24
|
artist: {
|
|
24
25
|
path: '/artists',
|
|
25
26
|
filtersDoc: 'artist: artist_name.',
|
|
26
27
|
fieldsDoc: 'artist — required: artist_name; optional: full_name, email, location, bios, isni, default_language, platform profile URLs.',
|
|
27
28
|
deleteNote: 'artist: refused while still referenced by releases or tracks.',
|
|
29
|
+
acceptsDeleteReplacement: false,
|
|
28
30
|
},
|
|
29
31
|
writer: {
|
|
30
32
|
path: '/writers',
|
|
31
33
|
filtersDoc: 'writer: name, ipi.',
|
|
32
34
|
fieldsDoc: 'writer — required: first_name, last_name; optional: middle_name, display_credits, email, country, pro, ipi, isni, publisher_id (or publisher_name/publisher_pro/publisher_ipi).',
|
|
33
|
-
deleteNote: 'writer: refused while still referenced by tracks.',
|
|
35
|
+
deleteNote: 'writer: refused while still referenced by tracks or artists, unless replace_with reassigns those credits.',
|
|
36
|
+
acceptsDeleteReplacement: true,
|
|
34
37
|
},
|
|
35
38
|
publisher: {
|
|
36
39
|
path: '/publishers',
|
|
37
40
|
filtersDoc: 'publisher: name, ipi.',
|
|
38
41
|
fieldsDoc: 'publisher — required: name; optional: ipi, pro, isni, controlled_publisher.',
|
|
39
|
-
deleteNote: 'publisher: refused while still referenced by
|
|
42
|
+
deleteNote: 'publisher: refused while still referenced by tracks or label default publishers, unless replace_with reassigns those credits.',
|
|
43
|
+
acceptsDeleteReplacement: true,
|
|
40
44
|
},
|
|
41
45
|
release: {
|
|
42
46
|
path: '/releases',
|
|
43
47
|
filtersDoc: 'release: label_id, is_live (1 = live only), barcode_number (UPC/EAN), cat.',
|
|
44
48
|
fieldsDoc: 'release — required on create: content_type, label_id, artists, titles, cat (catalog number), artwork_ai_usage, primary_genre_id; many optional fields (dates, copyright lines, genres, per-outlet URLs).',
|
|
45
49
|
deleteNote: 'release: only a never-submitted draft can be deleted.',
|
|
50
|
+
acceptsDeleteReplacement: false,
|
|
46
51
|
},
|
|
47
52
|
track: {
|
|
48
53
|
path: '/tracks',
|
|
49
54
|
filtersDoc: 'track: release_id, isrc.',
|
|
50
55
|
fieldsDoc: 'track — required on create: release_id, disc, track_num, composition_type, artists, audio_ai_usage, composition_ai_usage, commercial_samples, audio_language, contributors, and recording_country (ISO 3166-1 alpha-2, e.g. "US"); optional: titles, isrc, iswc, writers, publishers, splits, and more.',
|
|
51
56
|
deleteNote: 'track: refused once the release is no longer an editable draft.',
|
|
57
|
+
acceptsDeleteReplacement: false,
|
|
52
58
|
},
|
|
53
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* The entities whose DELETE accepts `replace_with`, derived from the registry so
|
|
62
|
+
* the tool schema, its description and its refusal message cannot disagree with
|
|
63
|
+
* the table or with each other.
|
|
64
|
+
*/
|
|
65
|
+
export const REPLACEMENT_ENTITIES = ENTITY_NAMES.filter((name) => ENTITIES[name].acceptsDeleteReplacement);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@labelgrid/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Shared LabelGrid public-API client: HTTP transport, uploads, content types, the catalog-entity registry, and redacting logging",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["labelgrid", "music-distribution", "api-client"],
|