kmcom-nuxt-layers 2.3.2 → 2.5.1

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 (57) hide show
  1. package/README.md +2 -0
  2. package/docs/ARCHITECTURE.md +4 -0
  3. package/layers/auth/app/middleware/auth.ts +4 -0
  4. package/layers/auth/nuxt.config.ts +31 -0
  5. package/layers/auth/package.json +15 -0
  6. package/layers/auth/server/routes/auth/github.get.ts +13 -0
  7. package/layers/auth/tsconfig.json +7 -0
  8. package/layers/baseline/app/components/BaselineStatus.vue +17 -0
  9. package/layers/baseline/app/plugins/baseline-status.client.ts +3 -0
  10. package/layers/baseline/nuxt.config.ts +24 -0
  11. package/layers/baseline/package.json +14 -0
  12. package/layers/baseline/tsconfig.json +7 -0
  13. package/layers/database/nuxt.config.ts +30 -0
  14. package/layers/database/package.json +16 -0
  15. package/layers/database/server/utils/drizzle.ts +5 -0
  16. package/layers/database/server/utils/sql.ts +7 -0
  17. package/layers/database/tsconfig.json +7 -0
  18. package/layers/metadata/app/composables/useMetadataItem.ts +11 -0
  19. package/layers/metadata/app/composables/useMetadataSearch.ts +25 -0
  20. package/layers/metadata/nuxt.config.ts +27 -0
  21. package/layers/metadata/package.json +12 -0
  22. package/layers/metadata/providers/comicvine/nuxt.config.ts +36 -0
  23. package/layers/metadata/providers/comicvine/package.json +12 -0
  24. package/layers/metadata/providers/comicvine/server/plugins/comicvine-register.ts +3 -0
  25. package/layers/metadata/providers/comicvine/server/utils/metadata-comicvine/client.ts +52 -0
  26. package/layers/metadata/providers/comicvine/server/utils/metadata-comicvine/normalise.ts +55 -0
  27. package/layers/metadata/providers/comicvine/server/utils/metadata-comicvine/provider.ts +39 -0
  28. package/layers/metadata/providers/comicvine/server/utils/metadata-comicvine/types.ts +36 -0
  29. package/layers/metadata/providers/comicvine/tsconfig.json +7 -0
  30. package/layers/metadata/providers/google-books/nuxt.config.ts +32 -0
  31. package/layers/metadata/providers/google-books/package.json +12 -0
  32. package/layers/metadata/providers/google-books/server/plugins/google-books-register.ts +3 -0
  33. package/layers/metadata/providers/google-books/server/utils/metadata-google-books/client.ts +23 -0
  34. package/layers/metadata/providers/google-books/server/utils/metadata-google-books/normalise.ts +39 -0
  35. package/layers/metadata/providers/google-books/server/utils/metadata-google-books/provider.ts +21 -0
  36. package/layers/metadata/providers/google-books/server/utils/metadata-google-books/types.ts +25 -0
  37. package/layers/metadata/providers/google-books/tsconfig.json +7 -0
  38. package/layers/metadata/providers/openlibrary/nuxt.config.ts +18 -0
  39. package/layers/metadata/providers/openlibrary/package.json +12 -0
  40. package/layers/metadata/providers/openlibrary/server/plugins/openlibrary-register.ts +3 -0
  41. package/layers/metadata/providers/openlibrary/server/utils/metadata-openlibrary/client.ts +30 -0
  42. package/layers/metadata/providers/openlibrary/server/utils/metadata-openlibrary/normalise.ts +81 -0
  43. package/layers/metadata/providers/openlibrary/server/utils/metadata-openlibrary/provider.ts +25 -0
  44. package/layers/metadata/providers/openlibrary/server/utils/metadata-openlibrary/types.ts +48 -0
  45. package/layers/metadata/providers/openlibrary/tsconfig.json +7 -0
  46. package/layers/metadata/server/api/metadata/lookup.get.ts +40 -0
  47. package/layers/metadata/server/api/metadata/providers.get.ts +7 -0
  48. package/layers/metadata/server/api/metadata/search.get.ts +17 -0
  49. package/layers/metadata/server/api/metadata/sync.post.ts +40 -0
  50. package/layers/metadata/server/utils/metadata/cache.ts +36 -0
  51. package/layers/metadata/server/utils/metadata/errors.ts +24 -0
  52. package/layers/metadata/server/utils/metadata/provider-registry.ts +15 -0
  53. package/layers/metadata/server/utils/metadata/search.ts +42 -0
  54. package/layers/metadata/tsconfig.json +7 -0
  55. package/package.json +16 -3
  56. package/types/auth.ts +7 -0
  57. package/types/index.ts +2 -0
@@ -0,0 +1,39 @@
1
+ import type { MetadataRecord, MetadataCreator } from '#layers/metadata/shared/types'
2
+ import type { GoogleBooksVolume } from './types'
3
+
4
+ // fallow-ignore-next-line complexity
5
+ export function normaliseGoogleBooksVolume(volume: GoogleBooksVolume): MetadataRecord {
6
+ const { volumeInfo } = volume
7
+
8
+ const creators: MetadataCreator[] = (volumeInfo.authors ?? []).map((name) => ({
9
+ name,
10
+ role: 'author',
11
+ }))
12
+
13
+ const isbn10 = volumeInfo.industryIdentifiers?.find((i) => i.type === 'ISBN_10')?.identifier
14
+ const isbn13 = volumeInfo.industryIdentifiers?.find((i) => i.type === 'ISBN_13')?.identifier
15
+
16
+ const cover = volumeInfo.imageLinks?.thumbnail ?? volumeInfo.imageLinks?.smallThumbnail
17
+
18
+ return {
19
+ id: `google-books:volume:${volume.id}`,
20
+ provider: 'google-books',
21
+ providerId: volume.id,
22
+ mediaType: 'book',
23
+ title: volumeInfo.title,
24
+ subtitle: volumeInfo.subtitle,
25
+ description: volumeInfo.description,
26
+ creators: creators.length ? creators : undefined,
27
+ publisher: volumeInfo.publisher,
28
+ publishedAt: volumeInfo.publishedDate,
29
+ coverUrl: cover ? cover.replace('http://', 'https://') : undefined,
30
+ identifiers: {
31
+ isbn10,
32
+ isbn13,
33
+ googleBooksId: volume.id,
34
+ },
35
+ sourceUrl: volumeInfo.canonicalVolumeLink ?? volumeInfo.infoLink,
36
+ raw: volume,
37
+ lastSyncedAt: new Date().toISOString(),
38
+ }
39
+ }
@@ -0,0 +1,21 @@
1
+ import type { MetadataProvider } from '#layers/metadata/shared/types'
2
+
3
+ export const googleBooksProvider: MetadataProvider = {
4
+ id: 'google-books',
5
+ label: 'Google Books',
6
+ mediaTypes: ['book', 'graphic-novel', 'collected-edition'],
7
+
8
+ async search({ query, limit = 10 }) {
9
+ const res = await searchGoogleBooks(query, limit)
10
+ return (res.items ?? []).map(normaliseGoogleBooksVolume)
11
+ },
12
+
13
+ async lookup({ providerId }) {
14
+ const volume = await fetchGoogleBooksVolume(providerId)
15
+ return normaliseGoogleBooksVolume(volume)
16
+ },
17
+
18
+ async sync({ providerId }) {
19
+ return (await googleBooksProvider.lookup!({ provider: 'google-books', providerId }))!
20
+ },
21
+ }
@@ -0,0 +1,25 @@
1
+ export type GoogleBooksVolumeInfo = {
2
+ title: string
3
+ subtitle?: string
4
+ authors?: string[]
5
+ publisher?: string
6
+ publishedDate?: string
7
+ description?: string
8
+ industryIdentifiers?: Array<{ type: 'ISBN_10' | 'ISBN_13' | string; identifier: string }>
9
+ pageCount?: number
10
+ imageLinks?: { thumbnail?: string; smallThumbnail?: string }
11
+ previewLink?: string
12
+ infoLink?: string
13
+ canonicalVolumeLink?: string
14
+ }
15
+
16
+ export type GoogleBooksVolume = {
17
+ id: string
18
+ selfLink: string
19
+ volumeInfo: GoogleBooksVolumeInfo
20
+ }
21
+
22
+ export type GoogleBooksSearchResponse = {
23
+ totalItems: number
24
+ items?: GoogleBooksVolume[]
25
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../../apps/playground/tsconfig.json",
3
+ "compilerOptions": {
4
+ "strict": true
5
+ },
6
+ "exclude": ["node_modules"]
7
+ }
@@ -0,0 +1,18 @@
1
+ export default defineNuxtConfig({
2
+ $meta: {
3
+ name: 'metadata-openlibrary',
4
+ },
5
+
6
+ extends: ['../../'],
7
+
8
+ alias: {
9
+ '#layers/metadata-openlibrary': import.meta.dirname,
10
+ },
11
+
12
+ compatibilityDate: '2026-06-30',
13
+
14
+ typescript: {
15
+ typeCheck: false,
16
+ strict: true,
17
+ },
18
+ })
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "kmcom-layer-metadata-openlibrary",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "./nuxt.config.ts",
6
+ "scripts": {
7
+ "dev": "nuxi dev",
8
+ "dev:playground": "PLAYGROUND_LAYERS=core,metadata,metadata-openlibrary pnpm -F playground dev",
9
+ "typecheck": "vue-tsc --noEmit -p ../../tsconfig.typecheck.json",
10
+ "lint": "eslint ."
11
+ }
12
+ }
@@ -0,0 +1,3 @@
1
+ export default defineNitroPlugin(() => {
2
+ registerProvider(openLibraryProvider)
3
+ })
@@ -0,0 +1,30 @@
1
+ import type { OpenLibrarySearchResponse, OpenLibraryWork, OpenLibraryEdition } from './types'
2
+
3
+ const BASE = 'https://openlibrary.org'
4
+
5
+ export function openLibraryCoverUrl(coverId: number, size: 'S' | 'M' | 'L' = 'M'): string {
6
+ return `https://covers.openlibrary.org/b/id/${coverId}-${size}.jpg`
7
+ }
8
+
9
+ export async function searchOpenLibrary(query: string, limit = 10): Promise<OpenLibrarySearchResponse> {
10
+ return $fetch<OpenLibrarySearchResponse>(`${BASE}/search.json`, {
11
+ query: { q: query, limit, fields: 'key,title,subtitle,author_name,author_key,publisher,first_publish_year,isbn,cover_i,number_of_pages_median' },
12
+ })
13
+ }
14
+
15
+ export async function lookupOpenLibraryWork(workId: string): Promise<OpenLibraryWork> {
16
+ return $fetch<OpenLibraryWork>(`${BASE}/works/${workId}.json`)
17
+ }
18
+
19
+ export async function lookupOpenLibraryEdition(editionId: string): Promise<OpenLibraryEdition> {
20
+ return $fetch<OpenLibraryEdition>(`${BASE}/books/${editionId}.json`)
21
+ }
22
+
23
+ export async function lookupByIsbn(isbn: string): Promise<OpenLibraryEdition | null> {
24
+ try {
25
+ return await $fetch<OpenLibraryEdition>(`${BASE}/isbn/${isbn}.json`)
26
+ }
27
+ catch {
28
+ return null
29
+ }
30
+ }
@@ -0,0 +1,81 @@
1
+ import type { MetadataRecord, MetadataCreator } from '#layers/metadata/shared/types'
2
+ import type { OpenLibrarySearchDoc, OpenLibraryWork, OpenLibraryEdition } from './types'
3
+
4
+ // fallow-ignore-next-line complexity
5
+ export function normaliseOpenLibrarySearchDoc(doc: OpenLibrarySearchDoc): MetadataRecord {
6
+ const creators: MetadataCreator[] = (doc.author_name ?? []).map((name, i) => ({
7
+ name,
8
+ role: 'author',
9
+ providerId: doc.author_key?.[i]?.replace('/authors/', '') ?? undefined,
10
+ }))
11
+
12
+ const id = doc.key.replace('/works/', '')
13
+
14
+ return {
15
+ id: `openlibrary:work:${id}`,
16
+ provider: 'openlibrary',
17
+ providerId: id,
18
+ mediaType: 'book',
19
+ title: doc.title,
20
+ subtitle: doc.subtitle,
21
+ creators: creators.length ? creators : undefined,
22
+ publisher: doc.publisher?.[0] ?? undefined,
23
+ publishedAt: doc.first_publish_year ? String(doc.first_publish_year) : undefined,
24
+ coverUrl: doc.cover_i ? openLibraryCoverUrl(doc.cover_i) : undefined,
25
+ identifiers: {
26
+ isbn10: doc.isbn?.find((i) => i.length === 10),
27
+ isbn13: doc.isbn?.find((i) => i.length === 13),
28
+ openLibraryId: id,
29
+ },
30
+ sourceUrl: `https://openlibrary.org${doc.key}`,
31
+ raw: doc,
32
+ lastSyncedAt: new Date().toISOString(),
33
+ }
34
+ }
35
+
36
+ // fallow-ignore-next-line complexity
37
+ export function normaliseOpenLibraryWork(work: OpenLibraryWork): MetadataRecord {
38
+ const id = work.key.replace('/works/', '')
39
+ const desc = typeof work.description === 'string' ? work.description : work.description?.value
40
+
41
+ return {
42
+ id: `openlibrary:work:${id}`,
43
+ provider: 'openlibrary',
44
+ providerId: id,
45
+ mediaType: 'book',
46
+ title: work.title,
47
+ subtitle: work.subtitle,
48
+ description: desc,
49
+ coverUrl: work.covers?.[0] ? openLibraryCoverUrl(work.covers[0]) : undefined,
50
+ publishedAt: work.first_publish_date,
51
+ identifiers: { openLibraryId: id },
52
+ sourceUrl: `https://openlibrary.org${work.key}`,
53
+ raw: work,
54
+ lastSyncedAt: new Date().toISOString(),
55
+ }
56
+ }
57
+
58
+ // fallow-ignore-next-line complexity
59
+ export function normaliseOpenLibraryEdition(edition: OpenLibraryEdition): MetadataRecord {
60
+ const id = edition.key.replace('/books/', '')
61
+
62
+ return {
63
+ id: `openlibrary:edition:${id}`,
64
+ provider: 'openlibrary',
65
+ providerId: id,
66
+ mediaType: 'book',
67
+ title: edition.title,
68
+ subtitle: edition.subtitle,
69
+ publisher: edition.publishers?.[0] ?? undefined,
70
+ publishedAt: edition.publish_date,
71
+ coverUrl: edition.covers?.[0] ? openLibraryCoverUrl(edition.covers[0]) : undefined,
72
+ identifiers: {
73
+ isbn10: edition.isbn_10?.[0],
74
+ isbn13: edition.isbn_13?.[0],
75
+ openLibraryId: id,
76
+ },
77
+ sourceUrl: `https://openlibrary.org${edition.key}`,
78
+ raw: edition,
79
+ lastSyncedAt: new Date().toISOString(),
80
+ }
81
+ }
@@ -0,0 +1,25 @@
1
+ import type { MetadataProvider } from '#layers/metadata/shared/types'
2
+
3
+ export const openLibraryProvider: MetadataProvider = {
4
+ id: 'openlibrary',
5
+ label: 'Open Library',
6
+ mediaTypes: ['book', 'graphic-novel', 'collected-edition'],
7
+
8
+ async search({ query, limit = 10 }) {
9
+ const res = await searchOpenLibrary(query, limit)
10
+ return res.docs.map(normaliseOpenLibrarySearchDoc)
11
+ },
12
+
13
+ async lookup({ providerId, resourceType = 'work' }) {
14
+ if (resourceType === 'edition') {
15
+ const edition = await lookupOpenLibraryEdition(providerId)
16
+ return normaliseOpenLibraryEdition(edition)
17
+ }
18
+ const work = await lookupOpenLibraryWork(providerId)
19
+ return normaliseOpenLibraryWork(work)
20
+ },
21
+
22
+ async sync({ providerId, resourceType = 'work' }) {
23
+ return (await openLibraryProvider.lookup!({ provider: 'openlibrary', providerId, resourceType }))!
24
+ },
25
+ }
@@ -0,0 +1,48 @@
1
+ export type OpenLibrarySearchDoc = {
2
+ key: string
3
+ title: string
4
+ subtitle?: string
5
+ author_name?: string[]
6
+ author_key?: string[]
7
+ publisher?: string[]
8
+ first_publish_year?: number
9
+ isbn?: string[]
10
+ cover_i?: number
11
+ number_of_pages_median?: number
12
+ language?: string[]
13
+ }
14
+
15
+ export type OpenLibrarySearchResponse = {
16
+ numFound: number
17
+ docs: OpenLibrarySearchDoc[]
18
+ }
19
+
20
+ export type OpenLibraryWork = {
21
+ key: string
22
+ title: string
23
+ subtitle?: string
24
+ description?: { value: string } | string
25
+ authors?: Array<{ author: { key: string }; type: { key: string } }>
26
+ first_publish_date?: string
27
+ covers?: number[]
28
+ subjects?: string[]
29
+ }
30
+
31
+ export type OpenLibraryEdition = {
32
+ key: string
33
+ title: string
34
+ subtitle?: string
35
+ publishers?: string[]
36
+ publish_date?: string
37
+ isbn_13?: string[]
38
+ isbn_10?: string[]
39
+ covers?: number[]
40
+ works?: Array<{ key: string }>
41
+ number_of_pages?: number
42
+ }
43
+
44
+ export type OpenLibraryAuthor = {
45
+ key: string
46
+ name: string
47
+ personal_name?: string
48
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../../apps/playground/tsconfig.json",
3
+ "compilerOptions": {
4
+ "strict": true
5
+ },
6
+ "exclude": ["node_modules"]
7
+ }
@@ -0,0 +1,40 @@
1
+ import type { MetadataProviderId } from '#layers/metadata/shared/types'
2
+
3
+ // fallow-ignore-next-line complexity
4
+ export default defineEventHandler(async (event) => {
5
+ const query = getQuery(event)
6
+
7
+ const providerId = String(query.providerId ?? '').trim()
8
+ const providerName = String(query.provider ?? '').trim() as MetadataProviderId
9
+ const resourceType = query.resourceType ? String(query.resourceType) : undefined
10
+
11
+ if (!providerName || !providerId) {
12
+ throw createError({ statusCode: 400, message: 'provider and providerId are required' })
13
+ }
14
+
15
+ const provider = getProvider(providerName)
16
+ if (!provider) throw createError({ statusCode: 404, message: `Provider '${providerName}' not registered` })
17
+
18
+ const cacheKey = makeCacheKey('metadata', providerName, resourceType ?? 'item', providerId)
19
+ const cached = await getCacheRecord(cacheKey)
20
+ if (cached) return cached.normalised
21
+
22
+ const result = await provider.lookup({ provider: providerName, providerId, resourceType })
23
+ if (!result) throw createError({ statusCode: 404, message: 'Not found' })
24
+
25
+ const now = new Date().toISOString()
26
+ await setCacheRecord(cacheKey, {
27
+ id: result.id,
28
+ provider: providerName,
29
+ providerId,
30
+ resourceType: resourceType ?? 'item',
31
+ mediaType: result.mediaType,
32
+ normalised: result,
33
+ raw: result.raw ?? null,
34
+ createdAt: now,
35
+ updatedAt: now,
36
+ lastSyncedAt: now,
37
+ })
38
+
39
+ return result
40
+ })
@@ -0,0 +1,7 @@
1
+ export default defineEventHandler(() => {
2
+ return getProviders().map((p) => ({
3
+ id: p.id,
4
+ label: p.label,
5
+ mediaTypes: p.mediaTypes,
6
+ }))
7
+ })
@@ -0,0 +1,17 @@
1
+ import type { MetadataMediaType, MetadataProviderId } from '#layers/metadata/shared/types'
2
+
3
+ // fallow-ignore-next-line complexity
4
+ export default defineEventHandler(async (event) => {
5
+ const query = getQuery(event)
6
+
7
+ const q = String(query.q ?? '').trim()
8
+ if (!q) throw createError({ statusCode: 400, message: 'q is required' })
9
+
10
+ const mediaType = query.mediaType as MetadataMediaType | undefined
11
+ const providers = query.providers
12
+ ? String(query.providers).split(',').filter(Boolean) as MetadataProviderId[]
13
+ : undefined
14
+ const limit = query.limit ? Number(query.limit) : 10
15
+
16
+ return searchMetadata({ query: q, mediaType, providers, limit })
17
+ })
@@ -0,0 +1,40 @@
1
+ import type { MetadataSyncInput, MetadataProviderId } from '#layers/metadata/shared/types'
2
+
3
+ // fallow-ignore-next-line complexity
4
+ export default defineEventHandler(async (event) => {
5
+ const body = await readBody<MetadataSyncInput>(event)
6
+
7
+ const { provider: providerName, providerId, resourceType, force } = body ?? {}
8
+ if (!providerName || !providerId) {
9
+ throw createError({ statusCode: 400, message: 'provider and providerId are required' })
10
+ }
11
+
12
+ const provider = getProvider(providerName as MetadataProviderId)
13
+ if (!provider) throw createError({ statusCode: 404, message: `Provider '${providerName}' not registered` })
14
+ if (!provider.sync) throw createError({ statusCode: 400, message: `Provider '${providerName}' does not support sync` })
15
+
16
+ const cacheKey = makeCacheKey('metadata', providerName, resourceType ?? 'item', providerId)
17
+
18
+ if (!force) {
19
+ const cached = await getCacheRecord(cacheKey)
20
+ if (cached) return { synced: false, record: cached.normalised }
21
+ }
22
+
23
+ const result = await provider.sync({ provider: providerName as MetadataProviderId, providerId, resourceType, force })
24
+ const now = new Date().toISOString()
25
+
26
+ await setCacheRecord(cacheKey, {
27
+ id: result.id,
28
+ provider: providerName as MetadataProviderId,
29
+ providerId,
30
+ resourceType: resourceType ?? 'item',
31
+ mediaType: result.mediaType,
32
+ normalised: result,
33
+ raw: result.raw ?? null,
34
+ createdAt: now,
35
+ updatedAt: now,
36
+ lastSyncedAt: now,
37
+ })
38
+
39
+ return { synced: true, record: result }
40
+ })
@@ -0,0 +1,36 @@
1
+ import type { MetadataCacheRecord } from '#layers/metadata/shared/types'
2
+
3
+ // Cache keys: metadata:{provider}:{resourceType}:{providerId}
4
+ // metadata-search:{provider}:{mediaType}:{queryHash}
5
+
6
+ export function makeCacheKey(...parts: string[]): string {
7
+ return parts.join(':')
8
+ }
9
+
10
+ export async function getCacheRecord(key: string): Promise<MetadataCacheRecord | null> {
11
+ const storage = useStorage('metadata')
12
+ return (await storage.getItem<MetadataCacheRecord>(key)) ?? null
13
+ }
14
+
15
+ export async function setCacheRecord(key: string, record: MetadataCacheRecord, ttl?: number): Promise<void> {
16
+ const storage = useStorage('metadata')
17
+ await storage.setItem(key, record, ttl ? { ttl } : undefined)
18
+ }
19
+
20
+ export async function getSearchCache<T>(key: string): Promise<T | null> {
21
+ const storage = useStorage('metadata')
22
+ return (await storage.getItem<T>(key)) ?? null
23
+ }
24
+
25
+ export async function setSearchCache<T>(key: string, value: T, ttl = 3600): Promise<void> {
26
+ const storage = useStorage('metadata')
27
+ await storage.setItem(key, value, { ttl })
28
+ }
29
+
30
+ export function hashQuery(query: string): string {
31
+ let h = 0
32
+ for (let i = 0; i < query.length; i++) {
33
+ h = (Math.imul(31, h) + query.charCodeAt(i)) | 0
34
+ }
35
+ return Math.abs(h).toString(36)
36
+ }
@@ -0,0 +1,24 @@
1
+ export class MetadataProviderError extends Error {
2
+ constructor(
3
+ public readonly provider: string,
4
+ message: string,
5
+ public readonly cause?: unknown,
6
+ ) {
7
+ super(`[metadata:${provider}] ${message}`)
8
+ this.name = 'MetadataProviderError'
9
+ }
10
+ }
11
+
12
+ export class MetadataNotFoundError extends Error {
13
+ constructor(provider: string, id: string) {
14
+ super(`[metadata:${provider}] Not found: ${id}`)
15
+ this.name = 'MetadataNotFoundError'
16
+ }
17
+ }
18
+
19
+ export class MetadataRateLimitError extends Error {
20
+ constructor(provider: string) {
21
+ super(`[metadata:${provider}] Rate limit exceeded`)
22
+ this.name = 'MetadataRateLimitError'
23
+ }
24
+ }
@@ -0,0 +1,15 @@
1
+ import type { MetadataProvider, MetadataProviderId } from '#layers/metadata/shared/types'
2
+
3
+ const _registry = new Map<MetadataProviderId, MetadataProvider>()
4
+
5
+ export function registerProvider(provider: MetadataProvider) {
6
+ _registry.set(provider.id, provider)
7
+ }
8
+
9
+ export function getProvider(id: MetadataProviderId): MetadataProvider | null {
10
+ return _registry.get(id) ?? null
11
+ }
12
+
13
+ export function getProviders(): MetadataProvider[] {
14
+ return [..._registry.values()]
15
+ }
@@ -0,0 +1,42 @@
1
+ import type { MetadataSearchInput, MetadataSearchResult } from '#layers/metadata/shared/types'
2
+
3
+ // fallow-ignore-next-line complexity
4
+ export async function searchMetadata(input: MetadataSearchInput): Promise<MetadataSearchResult[]> {
5
+ const all = getProviders()
6
+ const targets = input.providers?.length
7
+ ? all.filter((p) => input.providers!.includes(p.id))
8
+ : all.filter((p) => !input.mediaType || p.mediaTypes.includes(input.mediaType))
9
+
10
+ if (!targets.length) return []
11
+
12
+ const cacheKey = makeCacheKey(
13
+ 'metadata-search',
14
+ targets.map((p) => p.id).join('+'),
15
+ input.mediaType ?? 'any',
16
+ hashQuery(`${input.query}:${input.limit ?? 10}`),
17
+ )
18
+
19
+ const cached = await getSearchCache<MetadataSearchResult[]>(cacheKey)
20
+ if (cached) return cached
21
+
22
+ const settled = await Promise.allSettled(targets.map((p) => p.search(input)))
23
+
24
+ const results: MetadataSearchResult[] = []
25
+ for (const outcome of settled) {
26
+ if (outcome.status === 'fulfilled') results.push(...outcome.value)
27
+ }
28
+
29
+ const deduped = deduplicateResults(results)
30
+ await setSearchCache(cacheKey, deduped)
31
+ return deduped
32
+ }
33
+
34
+ function deduplicateResults(results: MetadataSearchResult[]): MetadataSearchResult[] {
35
+ const seen = new Set<string>()
36
+ return results.filter((r) => {
37
+ const key = `${r.title.toLowerCase()}:${r.publishedAt?.slice(0, 4) ?? ''}`
38
+ if (seen.has(key)) return false
39
+ seen.add(key)
40
+ return true
41
+ })
42
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../apps/playground/tsconfig.json",
3
+ "compilerOptions": {
4
+ "strict": true
5
+ },
6
+ "exclude": ["node_modules"]
7
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kmcom-nuxt-layers",
3
3
  "private": false,
4
- "version": "2.3.2",
4
+ "version": "2.5.1",
5
5
  "description": "Composable Nuxt 4 layers for building scalable Vue applications",
6
6
  "exports": {
7
7
  "./layers/core": "./layers/core/nuxt.config.ts",
@@ -21,11 +21,17 @@
21
21
  "./layers/shader": "./layers/shader/nuxt.config.ts",
22
22
  "./layers/mailer": "./layers/mailer/nuxt.config.ts",
23
23
  "./layers/forms": "./layers/forms/nuxt.config.ts",
24
+ "./layers/database": "./layers/database/nuxt.config.ts",
25
+ "./layers/auth": "./layers/auth/nuxt.config.ts",
24
26
  "./layers/theme": "./layers/theme/nuxt.config.ts",
25
27
  "./layers/content": "./layers/content/nuxt.config.ts",
26
28
  "./layers/feeds": "./layers/feeds/nuxt.config.ts",
27
29
  "./layers/routing": "./layers/routing/nuxt.config.ts",
28
- "./layers/starter": "./layers/starter/nuxt.config.ts"
30
+ "./layers/starter": "./layers/starter/nuxt.config.ts",
31
+ "./layers/metadata": "./layers/metadata/nuxt.config.ts",
32
+ "./layers/metadata/providers/comicvine": "./layers/metadata/providers/comicvine/nuxt.config.ts",
33
+ "./layers/metadata/providers/openlibrary": "./layers/metadata/providers/openlibrary/nuxt.config.ts",
34
+ "./layers/metadata/providers/google-books": "./layers/metadata/providers/google-books/nuxt.config.ts"
29
35
  },
30
36
  "files": [
31
37
  "layers/*/nuxt.config.ts",
@@ -36,6 +42,10 @@
36
42
  "layers/*/app/**",
37
43
  "layers/*/public/**",
38
44
  "layers/*/server/**",
45
+ "layers/*/providers/*/nuxt.config.ts",
46
+ "layers/*/providers/*/package.json",
47
+ "layers/*/providers/*/tsconfig.json",
48
+ "layers/*/providers/*/server/**",
39
49
  "types/**",
40
50
  "docs/"
41
51
  ],
@@ -159,6 +169,7 @@
159
169
  "@webgpu/glslang": "^0.0.15",
160
170
  "@webgpu/types": "^0.1.70",
161
171
  "browserslist": "^4.28.2",
172
+ "browserslist-config-baseline": "^0.5.0",
162
173
  "changesets": "^1.0.2",
163
174
  "csstype": "^3.2.3",
164
175
  "cypress": "^15.16.0",
@@ -207,7 +218,7 @@
207
218
  "last 2 Firefox major versions",
208
219
  "last 2 Safari major versions",
209
220
  "last 2 Edge major versions",
210
- "baseline widely available"
221
+ "extends browserslist-config-baseline"
211
222
  ],
212
223
  "dependencies": {
213
224
  "node-gyp": "^12.3.0",
@@ -228,6 +239,8 @@
228
239
  "dev:ui": "PLAYGROUND_LAYERS=core,ui pnpm -F playground dev",
229
240
  "dev:layout": "PLAYGROUND_LAYERS=core,ui,layout pnpm -F playground dev",
230
241
  "dev:motion": "PLAYGROUND_LAYERS=core,motion pnpm -F playground dev",
242
+ "dev:database": "PLAYGROUND_LAYERS=core,database pnpm -F playground dev",
243
+ "dev:auth": "PLAYGROUND_LAYERS=core,auth pnpm -F playground dev",
231
244
  "build": "turbo run build",
232
245
  "lint": "turbo run lint",
233
246
  "lint:fix": "turbo run lint -- --fix",
package/types/auth.ts ADDED
@@ -0,0 +1,7 @@
1
+ // fallow-ignore-next-line unused-type
2
+ export type SessionUser = {
3
+ provider: 'github'
4
+ providerId: string
5
+ username: string
6
+ avatarUrl?: string
7
+ }
package/types/index.ts CHANGED
@@ -17,3 +17,5 @@ export type {
17
17
  export type { ScrollLockEvent, LoadingStateEvent, LayerEvent } from './events'
18
18
  export type { MotionConfig, TransitionConfig } from './motion'
19
19
  export type { PublicRuntimeConfig } from './runtime'
20
+ // fallow-ignore-next-line unused-type
21
+ export type { SessionUser } from './auth'