@freqhole/api-client 0.1.30

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.
Files changed (45) hide show
  1. package/auth-test.html +651 -0
  2. package/package.json +42 -0
  3. package/src/AdminClient.ts +154 -0
  4. package/src/CharnelLocalTransport.ts +598 -0
  5. package/src/CharnelTransport.ts +616 -0
  6. package/src/FreqholeClient.ts +261 -0
  7. package/src/WasmTransport.ts +1077 -0
  8. package/src/automerge/IrohNetworkAdapter.ts +727 -0
  9. package/src/automerge/index.ts +14 -0
  10. package/src/codegen/admin_commands.ts +61 -0
  11. package/src/codegen/routes.ts +213 -0
  12. package/src/codegen/schema.ts +6721 -0
  13. package/src/domains/admin.ts +97 -0
  14. package/src/domains/app.ts +98 -0
  15. package/src/domains/auth.ts +191 -0
  16. package/src/domains/favorites.types.ts +52 -0
  17. package/src/domains/jobs.ts +48 -0
  18. package/src/domains/music.ts +1935 -0
  19. package/src/domains/types.ts +19 -0
  20. package/src/domains/upload.ts +214 -0
  21. package/src/index.ts +48 -0
  22. package/src/permissions.ts +468 -0
  23. package/src/playlistz/convert.ts +267 -0
  24. package/src/playlistz/index.ts +6 -0
  25. package/src/playlistz/mutations.ts +181 -0
  26. package/src/playlistz/protocol.ts +222 -0
  27. package/src/playlistz/schema.ts +138 -0
  28. package/src/playlistz/shareLink.ts +76 -0
  29. package/src/storage/blobs.ts +272 -0
  30. package/src/storage/identity.ts +190 -0
  31. package/src/storage/index.ts +5 -0
  32. package/src/storage/webLocks.ts +92 -0
  33. package/src/test/automerge.ts +796 -0
  34. package/src/test/coverage.ts +105 -0
  35. package/src/test/fixtures.ts +460 -0
  36. package/src/test/integration.ts +320 -0
  37. package/src/test/playlistz.ts +631 -0
  38. package/src/test/stateful.ts +561 -0
  39. package/src/test/storage.ts +443 -0
  40. package/src/test.ts +75 -0
  41. package/src/transport.ts +391 -0
  42. package/src/utils.ts +79 -0
  43. package/src/webauthn.ts +228 -0
  44. package/tsconfig.json +22 -0
  45. package/vite.config.js +9 -0
@@ -0,0 +1,105 @@
1
+ // client method coverage validation - compile-time route safety
2
+ //
3
+ // purpose: ensure every generated route has a corresponding typed method in FreqholeClient
4
+ //
5
+ // what this tests:
6
+ // - every route in codegen/routes.ts has a method in client.app/auth/music
7
+ // - naming convention consistency (snake_case routes -> camelCase methods)
8
+ //
9
+ // runs at compile time - no server or API_KEY needed
10
+ // helps catch missing methods when new routes are added to the server
11
+ import { routes } from "../codegen/routes.js";
12
+ import { createHttpClient } from "../FreqholeClient.js";
13
+
14
+ // create a client to inspect its methods
15
+ const client = createHttpClient("http://localhost:8080");
16
+
17
+ // map of domain -> client namespace
18
+ const clientDomains = {
19
+ admin: client.admin,
20
+ app: client.app,
21
+ auth: client.auth,
22
+ music: client.music,
23
+ };
24
+
25
+ // convert route names to expected method names (snake_case -> camelCase)
26
+ function toMethodName(routeName: string): string {
27
+ return routeName.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
28
+ }
29
+
30
+ // convert method names back to route names (camelCase -> snake_case)
31
+ function toRouteName(methodName: string): string {
32
+ return methodName.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
33
+ }
34
+
35
+ export async function validateWrapperCoverage() {
36
+ let passed = 0;
37
+ let failed = 0;
38
+ const errors: string[] = [];
39
+
40
+ console.log("validating client method coverage...\n");
41
+
42
+ // check each route has a client method
43
+ for (const [domain, domainRoutes] of Object.entries(routes)) {
44
+ const clientNamespace = clientDomains[domain as keyof typeof clientDomains];
45
+
46
+ if (!clientNamespace) {
47
+ errors.push(`✗ no client namespace for domain '${domain}'`);
48
+ failed++;
49
+ continue;
50
+ }
51
+
52
+ for (const routeName of Object.keys(domainRoutes)) {
53
+ const expectedMethodName = toMethodName(routeName);
54
+
55
+ if (!(expectedMethodName in clientNamespace)) {
56
+ errors.push(
57
+ `✗ ${domain}.${routeName}: missing client method '${expectedMethodName}' in client.${domain}`,
58
+ );
59
+ failed++;
60
+ } else {
61
+ console.log(`✓ client method exists for ${domain}.${routeName}`);
62
+ passed++;
63
+ }
64
+ }
65
+ }
66
+
67
+ // check for extra methods (methods without routes)
68
+ for (const [domain, clientNamespace] of Object.entries(clientDomains)) {
69
+ const domainRoutes = routes[domain as keyof typeof routes];
70
+
71
+ if (!domainRoutes) {
72
+ errors.push(`✗ client domain '${domain}' has no routes`);
73
+ failed++;
74
+ continue;
75
+ }
76
+
77
+ const methodNames = Object.keys(clientNamespace).filter(
78
+ (key) => typeof (clientNamespace as any)[key] === "function",
79
+ );
80
+
81
+ for (const methodName of methodNames) {
82
+ const routeName = toRouteName(methodName);
83
+
84
+ if (!(routeName in domainRoutes)) {
85
+ errors.push(
86
+ `✗ client.${domain}.${methodName}: method has no corresponding route '${routeName}'`,
87
+ );
88
+ failed++;
89
+ } else {
90
+ console.log(`✓ route exists for client.${domain}.${methodName}`);
91
+ passed++;
92
+ }
93
+ }
94
+ }
95
+
96
+ // print errors at the end for visibility
97
+ if (errors.length > 0) {
98
+ console.log("\nerrors found:\n");
99
+ errors.forEach((err) => console.log(err));
100
+ }
101
+
102
+ console.log(`\n${passed} passed, ${failed} failed\n`);
103
+
104
+ return { passed, failed };
105
+ }
@@ -0,0 +1,460 @@
1
+ // test fixtures - typed minimal valid data for all schema types
2
+ import type * as s from "../codegen/schema.js";
3
+
4
+ // ============================================================================
5
+ // Constants & IDs
6
+ // ============================================================================
7
+
8
+ export const PLACEHOLDER_ID = "placeholder-id";
9
+
10
+ export const testIds = {
11
+ song: "test-song-id",
12
+ album: "test-album-id",
13
+ artist: "test-artist-id",
14
+ playlist: "test-playlist-id",
15
+ genre: "test-genre-id",
16
+ subGenre: "test-sub-genre-id",
17
+ tag: "test-tag-id",
18
+ job: "test-job-id",
19
+ blob: "test-blob-id",
20
+ user: "test-user-id",
21
+ };
22
+
23
+ // ============================================================================
24
+ // Common Query Params
25
+ // ============================================================================
26
+
27
+ export const queryParams: s.QueryParams = {
28
+ q: null,
29
+ search_fields: null,
30
+ filters: {},
31
+ sort_by: null,
32
+ sort_direction: null,
33
+ limit: 10,
34
+ offset: 0,
35
+ user_id: null,
36
+ favorites_only: null,
37
+ min_rating: null,
38
+ };
39
+
40
+ // ============================================================================
41
+ // Request Fixtures
42
+ // ============================================================================
43
+
44
+ // Artists
45
+ export const createArtistRequest: s.CreateArtistRequest = {
46
+ name: "Test Artist",
47
+ created_by: null,
48
+ };
49
+
50
+ export const getArtistRequest: s.GetArtistRequest = {
51
+ id: PLACEHOLDER_ID,
52
+ };
53
+
54
+ export const deleteArtistRequest: s.DeleteArtistRequest = {
55
+ id: PLACEHOLDER_ID,
56
+ user_id: PLACEHOLDER_ID,
57
+ };
58
+
59
+ // Albums
60
+ export const getAlbumRequest: s.GetAlbumRequest = {
61
+ id: PLACEHOLDER_ID,
62
+ };
63
+
64
+ export const deleteAlbumRequest: s.DeleteAlbumRequest = {
65
+ id: PLACEHOLDER_ID,
66
+ user_id: PLACEHOLDER_ID,
67
+ };
68
+
69
+ // Songs
70
+ export const recentSongsRequest: s.RecentSongsRequest = {
71
+ limit: 10,
72
+ };
73
+
74
+ export const updateSongsRequest: s.UpdateSongsRequest = {
75
+ song_ids: [PLACEHOLDER_ID],
76
+ user_id: null,
77
+ updated_by: null,
78
+ title: "Updated Title",
79
+ track_number: null,
80
+ disc_number: null,
81
+ duration: null,
82
+ track_artist: null,
83
+ year: null,
84
+ bpm: null,
85
+ lyrics: null,
86
+ metadata: null,
87
+ artist: null,
88
+ artist_id: null,
89
+ artist_name: null,
90
+ album: null,
91
+ album_id: null,
92
+ album_title: null,
93
+ album_type: null,
94
+ populate_track_artist: null,
95
+ aggregate_album_images: null,
96
+ release_date: null,
97
+ label: null,
98
+ genre: null,
99
+ add_tags: null,
100
+ remove_tags: null,
101
+ replace_tags: null,
102
+ set_favorite: null,
103
+ favorite_song: false,
104
+ favorite_artist: false,
105
+ favorite_album: false,
106
+ set_rating: null,
107
+ rate_song: null,
108
+ rate_artist: null,
109
+ rate_album: null,
110
+ entity_urls: null,
111
+ };
112
+
113
+ export const deleteSongRequest: s.DeleteSongRequest = {
114
+ id: PLACEHOLDER_ID,
115
+ user_id: PLACEHOLDER_ID,
116
+ };
117
+
118
+ // Playlists
119
+ export const createPlaylistRequest: s.CreatePlaylistRequest = {
120
+ id: null,
121
+ title: "Test Playlist",
122
+ description: null,
123
+ is_public: false,
124
+ created_by_id: null,
125
+ };
126
+
127
+ export const updatePlaylistRequest: s.UpdatePlaylistRequest = {
128
+ playlist_id: PLACEHOLDER_ID,
129
+ title: "Updated Playlist",
130
+ description: "Updated description",
131
+ is_public: null,
132
+ entity_urls: null,
133
+ updated_by: null,
134
+ };
135
+
136
+ export const deletePlaylistRequest: s.DeletePlaylistRequest = {
137
+ playlist_id: PLACEHOLDER_ID,
138
+ };
139
+
140
+ export const queryPlaylistSongsRequest: s.QueryPlaylistSongsRequest = {
141
+ playlist_id: PLACEHOLDER_ID,
142
+ q: null,
143
+ sort_by: null,
144
+ sort_direction: null,
145
+ limit: 10,
146
+ offset: 0,
147
+ };
148
+
149
+ export const addSongsToPlaylistRequest: s.AddSongsToPlaylistRequest = {
150
+ playlist_id: PLACEHOLDER_ID,
151
+ song_ids: [PLACEHOLDER_ID],
152
+ };
153
+
154
+ export const removeSongsFromPlaylistRequest: s.RemoveSongsFromPlaylistRequest =
155
+ {
156
+ playlist_id: PLACEHOLDER_ID,
157
+ song_ids: [PLACEHOLDER_ID],
158
+ };
159
+
160
+ export const reorderPlaylistSongsRequest: s.ReorderPlaylistSongsRequest = {
161
+ playlist_id: PLACEHOLDER_ID,
162
+ song_ids: ["song-1", "song-2"],
163
+ new_position: 0,
164
+ };
165
+
166
+ export const removePlaylistThumbnailRequest: s.RemovePlaylistThumbnailRequest =
167
+ {
168
+ playlist_id: PLACEHOLDER_ID,
169
+ cleanup_blob: false,
170
+ deleted_by: null,
171
+ };
172
+
173
+ // Favorites
174
+ export const listFavoritesRequest: s.ListFavoritesRequest = {
175
+ user_id: PLACEHOLDER_ID,
176
+ target_type: "song",
177
+ limit: 10,
178
+ offset: 0,
179
+ };
180
+
181
+ export const setFavoriteRequest: s.SetFavoriteRequest = {
182
+ user_id: PLACEHOLDER_ID,
183
+ target_type: "song",
184
+ target_id: PLACEHOLDER_ID,
185
+ is_favorite: true,
186
+ };
187
+
188
+ // Ratings
189
+ export const setRatingRequest: s.SetRatingRequest = {
190
+ user_id: PLACEHOLDER_ID,
191
+ target_type: "song",
192
+ target_id: PLACEHOLDER_ID,
193
+ rating: 5,
194
+ };
195
+
196
+ export const removeRatingRequest: s.RemoveRatingRequest = {
197
+ user_id: PLACEHOLDER_ID,
198
+ target_type: "song",
199
+ target_id: PLACEHOLDER_ID,
200
+ };
201
+
202
+ export const getRatingStatsRequest: s.GetRatingStatsRequest = {
203
+ target_type: "song",
204
+ target_id: PLACEHOLDER_ID,
205
+ };
206
+
207
+ // Tags
208
+ export const queryTagsRequest: s.QueryTagsRequest = {
209
+ search: "test",
210
+ };
211
+
212
+ export const getTagRequest: s.GetTagRequest = {
213
+ tag_id: PLACEHOLDER_ID,
214
+ };
215
+
216
+ export const deleteTagRequest: s.DeleteTagRequest = {
217
+ tag_id: PLACEHOLDER_ID,
218
+ deleted_by: null,
219
+ };
220
+
221
+ export const getAlbumsTagsRequest: s.GetAlbumsTagsRequest = {
222
+ album_ids: [PLACEHOLDER_ID],
223
+ };
224
+
225
+ export const addAlbumsTagsRequest: s.AddAlbumsTagsRequest = {
226
+ album_ids: [PLACEHOLDER_ID],
227
+ tag_ids: [PLACEHOLDER_ID],
228
+ tag_names: [],
229
+ };
230
+
231
+ export const removeAlbumsTagsRequest: s.RemoveAlbumsTagsRequest = {
232
+ album_ids: [PLACEHOLDER_ID],
233
+ tag_ids: [PLACEHOLDER_ID],
234
+ };
235
+
236
+ export const replaceAlbumsTagsRequest: s.ReplaceAlbumsTagsRequest = {
237
+ album_ids: [PLACEHOLDER_ID],
238
+ tag_ids: [PLACEHOLDER_ID],
239
+ };
240
+
241
+ // Images
242
+ export const deleteImageRequest: s.DeleteImageRequest = {
243
+ entity_type: "album",
244
+ entity_id: PLACEHOLDER_ID,
245
+ blob_id: testIds.blob,
246
+ };
247
+
248
+ // Analytics
249
+ export const recordPlayRequest: s.RecordPlayRequest = {
250
+ media_blob_id: PLACEHOLDER_ID,
251
+ song_id: PLACEHOLDER_ID,
252
+ session_id: null,
253
+ event_data: null,
254
+ };
255
+
256
+ export const songAnalyticsRequest: s.SongAnalyticsRequest = {
257
+ song_id: PLACEHOLDER_ID,
258
+ };
259
+
260
+ export const listeningHistoryRequest: s.ListeningHistoryRequest = {
261
+ user_id: null,
262
+ limit: 10,
263
+ offset: 0,
264
+ };
265
+
266
+ export const topSongsRequest: s.TopSongsRequest = {
267
+ limit: 10,
268
+ days: 7,
269
+ };
270
+
271
+ export const topArtistsRequest: s.TopArtistsRequest = {
272
+ limit: 10,
273
+ days: 7,
274
+ };
275
+
276
+ export const topAlbumsRequest: s.TopAlbumsRequest = {
277
+ limit: 10,
278
+ days: 7,
279
+ };
280
+
281
+ export const feedRequest: s.FeedRequest = {
282
+ limit: 10,
283
+ offset: 0,
284
+ feed_types: null,
285
+ user_id: null,
286
+ };
287
+
288
+ // MusicBrainz
289
+ export const searchReleasesRequest: s.SearchReleasesRequest = {
290
+ artist: "test artist",
291
+ release: "test release",
292
+ limit: null,
293
+ offset: null,
294
+ };
295
+
296
+ export const getReleaseRequest: s.GetReleaseRequest = {
297
+ mbid: PLACEHOLDER_ID,
298
+ };
299
+
300
+ // Jobs
301
+ export const listJobsRequest: s.ListJobsRequest = {
302
+ session_id: null,
303
+ status: null,
304
+ limit: 10,
305
+ offset: 0,
306
+ };
307
+
308
+ export const getJobsStatusRequest: s.GetJobsStatusRequest = {
309
+ job_ids: [PLACEHOLDER_ID],
310
+ };
311
+
312
+ // Fetch
313
+ export const fetchMediaParams: s.FetchMediaParams = {
314
+ url: "https://example.com/video",
315
+ user_id: null,
316
+ };
317
+
318
+ // Auth
319
+ export const registerStartRequest: s.RegisterStartRequest = {
320
+ username: "testuser",
321
+ invite_code: null,
322
+ };
323
+
324
+ export const startLoginRequest: s.StartLoginRequest = {
325
+ username: "testuser",
326
+ };
327
+
328
+ export const redeemInviteRequest: s.RedeemInviteRequest = {
329
+ invite_code: "test-invite-code",
330
+ username: "testuser",
331
+ node_id: null,
332
+ };
333
+
334
+ // ============================================================================
335
+ // Helper Functions
336
+ // ============================================================================
337
+
338
+ // merge fixture with overrides, replacing PLACEHOLDER_ID with real IDs
339
+ export function merge<T extends Record<string, any>>(
340
+ fixture: T,
341
+ overrides: Partial<T>,
342
+ ): T {
343
+ const merged = { ...fixture, ...overrides };
344
+
345
+ // recursively replace PLACEHOLDER_ID in the merged object
346
+ return JSON.parse(
347
+ JSON.stringify(merged).replace(
348
+ new RegExp(PLACEHOLDER_ID, "g"),
349
+ (overrides.id as string) || PLACEHOLDER_ID,
350
+ ),
351
+ );
352
+ }
353
+
354
+ // create a fixture with specific ID
355
+ export function withId<T extends { id?: string }>(fixture: T, id: string): T {
356
+ return { ...fixture, id };
357
+ }
358
+
359
+ // create a fixture with specific entity_id
360
+ export function withEntityId<T extends { entity_id?: string }>(
361
+ fixture: T,
362
+ entityId: string,
363
+ ): T {
364
+ return { ...fixture, entity_id: entityId };
365
+ }
366
+
367
+ // create a fixture with specific playlist_id
368
+ export function withPlaylistId<T extends { playlist_id?: string }>(
369
+ fixture: T,
370
+ playlistId: string,
371
+ ): T {
372
+ return { ...fixture, playlist_id: playlistId };
373
+ }
374
+
375
+ // create a fixture with specific album_id
376
+ export function withAlbumId<T extends { album_id?: string }>(
377
+ fixture: T,
378
+ albumId: string,
379
+ ): T {
380
+ return { ...fixture, album_id: albumId };
381
+ }
382
+
383
+ // ============================================================================
384
+ // Fixture Collections (for convenience)
385
+ // ============================================================================
386
+
387
+ export const fixtures = {
388
+ // common
389
+ queryParams,
390
+
391
+ // artists
392
+ createArtist: createArtistRequest,
393
+ getArtist: getArtistRequest,
394
+ deleteArtist: deleteArtistRequest,
395
+
396
+ // albums
397
+ getAlbum: getAlbumRequest,
398
+ deleteAlbum: deleteAlbumRequest,
399
+
400
+ // songs
401
+ recentSongs: recentSongsRequest,
402
+ updateSongs: updateSongsRequest,
403
+ deleteSong: deleteSongRequest,
404
+
405
+ // playlists
406
+ createPlaylist: createPlaylistRequest,
407
+ updatePlaylist: updatePlaylistRequest,
408
+ deletePlaylist: deletePlaylistRequest,
409
+ queryPlaylistSongs: queryPlaylistSongsRequest,
410
+ addSongsToPlaylist: addSongsToPlaylistRequest,
411
+ removeSongsFromPlaylist: removeSongsFromPlaylistRequest,
412
+ reorderPlaylistSongs: reorderPlaylistSongsRequest,
413
+ removePlaylistThumbnail: removePlaylistThumbnailRequest,
414
+
415
+ // favorites
416
+ listFavorites: listFavoritesRequest,
417
+ setFavorite: setFavoriteRequest,
418
+
419
+ // ratings
420
+ setRating: setRatingRequest,
421
+ removeRating: removeRatingRequest,
422
+ getRatingStats: getRatingStatsRequest,
423
+
424
+ // tags
425
+ queryTags: queryTagsRequest,
426
+ getTag: getTagRequest,
427
+ deleteTag: deleteTagRequest,
428
+ getAlbumTags: getAlbumsTagsRequest,
429
+ addAlbumTags: addAlbumsTagsRequest,
430
+ removeAlbumTags: removeAlbumsTagsRequest,
431
+ replaceAlbumTags: replaceAlbumsTagsRequest,
432
+
433
+ // images
434
+ deleteImage: deleteImageRequest,
435
+
436
+ // analytics
437
+ recordPlay: recordPlayRequest,
438
+ songAnalytics: songAnalyticsRequest,
439
+ listeningHistory: listeningHistoryRequest,
440
+ topSongs: topSongsRequest,
441
+ topArtists: topArtistsRequest,
442
+ topAlbums: topAlbumsRequest,
443
+ feed: feedRequest,
444
+
445
+ // musicbrainz
446
+ searchReleases: searchReleasesRequest,
447
+ getRelease: getReleaseRequest,
448
+
449
+ // jobs
450
+ listJobs: listJobsRequest,
451
+ getJobsStatus: getJobsStatusRequest,
452
+
453
+ // fetch
454
+ fetchMedia: fetchMediaParams,
455
+
456
+ // auth
457
+ registerStart: registerStartRequest,
458
+ startLogin: startLoginRequest,
459
+ redeemInvite: redeemInviteRequest,
460
+ };