kmcom-nuxt-layers 2.3.2 → 2.5.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/README.md +2 -0
- package/docs/ARCHITECTURE.md +4 -0
- package/layers/auth/app/middleware/auth.ts +4 -0
- package/layers/auth/nuxt.config.ts +31 -0
- package/layers/auth/package.json +15 -0
- package/layers/auth/server/routes/auth/github.get.ts +13 -0
- package/layers/auth/tsconfig.json +7 -0
- package/layers/baseline/app/components/BaselineStatus.vue +17 -0
- package/layers/baseline/app/plugins/baseline-status.client.ts +3 -0
- package/layers/baseline/nuxt.config.ts +24 -0
- package/layers/baseline/package.json +14 -0
- package/layers/baseline/tsconfig.json +7 -0
- package/layers/database/nuxt.config.ts +30 -0
- package/layers/database/package.json +16 -0
- package/layers/database/server/utils/drizzle.ts +5 -0
- package/layers/database/server/utils/sql.ts +7 -0
- package/layers/database/tsconfig.json +7 -0
- package/layers/metadata/app/composables/useMetadataItem.ts +11 -0
- package/layers/metadata/app/composables/useMetadataSearch.ts +25 -0
- package/layers/metadata/nuxt.config.ts +27 -0
- package/layers/metadata/package.json +12 -0
- package/layers/metadata/server/api/metadata/lookup.get.ts +40 -0
- package/layers/metadata/server/api/metadata/providers.get.ts +7 -0
- package/layers/metadata/server/api/metadata/search.get.ts +17 -0
- package/layers/metadata/server/api/metadata/sync.post.ts +40 -0
- package/layers/metadata/server/utils/metadata/cache.ts +36 -0
- package/layers/metadata/server/utils/metadata/errors.ts +24 -0
- package/layers/metadata/server/utils/metadata/provider-registry.ts +15 -0
- package/layers/metadata/server/utils/metadata/search.ts +42 -0
- package/layers/metadata/tsconfig.json +7 -0
- package/layers/metadata-comicvine/nuxt.config.ts +36 -0
- package/layers/metadata-comicvine/package.json +12 -0
- package/layers/metadata-comicvine/server/plugins/comicvine-register.ts +3 -0
- package/layers/metadata-comicvine/server/utils/metadata-comicvine/client.ts +52 -0
- package/layers/metadata-comicvine/server/utils/metadata-comicvine/normalise.ts +55 -0
- package/layers/metadata-comicvine/server/utils/metadata-comicvine/provider.ts +39 -0
- package/layers/metadata-comicvine/server/utils/metadata-comicvine/types.ts +36 -0
- package/layers/metadata-comicvine/tsconfig.json +7 -0
- package/layers/metadata-google-books/nuxt.config.ts +32 -0
- package/layers/metadata-google-books/package.json +12 -0
- package/layers/metadata-google-books/server/plugins/google-books-register.ts +3 -0
- package/layers/metadata-google-books/server/utils/metadata-google-books/client.ts +23 -0
- package/layers/metadata-google-books/server/utils/metadata-google-books/normalise.ts +39 -0
- package/layers/metadata-google-books/server/utils/metadata-google-books/provider.ts +21 -0
- package/layers/metadata-google-books/server/utils/metadata-google-books/types.ts +25 -0
- package/layers/metadata-google-books/tsconfig.json +7 -0
- package/layers/metadata-openlibrary/nuxt.config.ts +18 -0
- package/layers/metadata-openlibrary/package.json +12 -0
- package/layers/metadata-openlibrary/server/plugins/openlibrary-register.ts +3 -0
- package/layers/metadata-openlibrary/server/utils/metadata-openlibrary/client.ts +30 -0
- package/layers/metadata-openlibrary/server/utils/metadata-openlibrary/normalise.ts +81 -0
- package/layers/metadata-openlibrary/server/utils/metadata-openlibrary/provider.ts +25 -0
- package/layers/metadata-openlibrary/server/utils/metadata-openlibrary/types.ts +48 -0
- package/layers/metadata-openlibrary/tsconfig.json +7 -0
- package/package.json +12 -3
- package/types/auth.ts +7 -0
- package/types/index.ts +2 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ComicVineApiResponse } from './types'
|
|
2
|
+
|
|
3
|
+
const FIELD_LIST_ISSUE = 'id,name,issue_number,description,deck,cover_date,store_date,site_detail_url,image,volume,person_credits,publisher'
|
|
4
|
+
const FIELD_LIST_VOLUME = 'id,name,description,deck,start_year,site_detail_url,image,publisher,people'
|
|
5
|
+
|
|
6
|
+
function getConfig() {
|
|
7
|
+
const config = useRuntimeConfig()
|
|
8
|
+
return config.metadataComicvine
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function comicVineFetch<T>(path: string, params: Record<string, string>): Promise<ComicVineApiResponse<T>> {
|
|
12
|
+
const { apiKey, baseUrl } = getConfig()
|
|
13
|
+
|
|
14
|
+
const url = new URL(`${baseUrl}${path}`)
|
|
15
|
+
url.searchParams.set('api_key', apiKey)
|
|
16
|
+
url.searchParams.set('format', 'json')
|
|
17
|
+
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v)
|
|
18
|
+
|
|
19
|
+
const res = await $fetch<ComicVineApiResponse<T>>(url.toString())
|
|
20
|
+
if (res.status_code !== 1) throw new MetadataProviderError('comicvine', `API error: ${res.error}`)
|
|
21
|
+
return res
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function searchComicVineIssues(query: string, limit = 10) {
|
|
25
|
+
return comicVineFetch<import('./types').ComicVineIssue[]>('/search/', {
|
|
26
|
+
query,
|
|
27
|
+
resources: 'issue',
|
|
28
|
+
limit: String(limit),
|
|
29
|
+
field_list: FIELD_LIST_ISSUE,
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function searchComicVineVolumes(query: string, limit = 10) {
|
|
34
|
+
return comicVineFetch<import('./types').ComicVineVolume[]>('/search/', {
|
|
35
|
+
query,
|
|
36
|
+
resources: 'volume',
|
|
37
|
+
limit: String(limit),
|
|
38
|
+
field_list: FIELD_LIST_VOLUME,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function fetchComicVineIssue(id: string) {
|
|
43
|
+
return comicVineFetch<import('./types').ComicVineIssue>(`/issue/4000-${id}/`, {
|
|
44
|
+
field_list: FIELD_LIST_ISSUE,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function fetchComicVineVolume(id: string) {
|
|
49
|
+
return comicVineFetch<import('./types').ComicVineVolume>(`/volume/4050-${id}/`, {
|
|
50
|
+
field_list: FIELD_LIST_VOLUME,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { MetadataRecord, MetadataCreator } from '#layers/metadata/shared/types'
|
|
2
|
+
import type { ComicVineIssue, ComicVineVolume } from './types'
|
|
3
|
+
|
|
4
|
+
// fallow-ignore-next-line complexity
|
|
5
|
+
export function normaliseComicVineIssue(issue: ComicVineIssue): MetadataRecord {
|
|
6
|
+
const creators: MetadataCreator[] = (issue.person_credits ?? []).map((p) => ({
|
|
7
|
+
name: p.name,
|
|
8
|
+
role: p.role,
|
|
9
|
+
providerId: String(p.id),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
id: `comicvine:issue:${issue.id}`,
|
|
14
|
+
provider: 'comicvine',
|
|
15
|
+
providerId: String(issue.id),
|
|
16
|
+
mediaType: 'comic',
|
|
17
|
+
title: issue.volume?.name ? `${issue.volume.name} #${issue.issue_number}` : (issue.name ?? `Issue #${issue.issue_number}`),
|
|
18
|
+
subtitle: issue.name ?? undefined,
|
|
19
|
+
description: issue.deck ?? issue.description ?? undefined,
|
|
20
|
+
creators: creators.length ? creators : undefined,
|
|
21
|
+
publisher: issue.publisher?.name ?? undefined,
|
|
22
|
+
publishedAt: issue.cover_date ?? issue.store_date ?? undefined,
|
|
23
|
+
coverUrl: issue.image?.medium_url ?? issue.image?.original_url ?? undefined,
|
|
24
|
+
identifiers: { comicVineId: String(issue.id) },
|
|
25
|
+
sourceUrl: issue.site_detail_url,
|
|
26
|
+
raw: issue,
|
|
27
|
+
lastSyncedAt: new Date().toISOString(),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// fallow-ignore-next-line complexity
|
|
32
|
+
export function normaliseComicVineVolume(volume: ComicVineVolume): MetadataRecord {
|
|
33
|
+
const creators: MetadataCreator[] = (volume.people ?? []).map((p) => ({
|
|
34
|
+
name: p.name,
|
|
35
|
+
role: p.role,
|
|
36
|
+
providerId: String(p.id),
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
id: `comicvine:volume:${volume.id}`,
|
|
41
|
+
provider: 'comicvine',
|
|
42
|
+
providerId: String(volume.id),
|
|
43
|
+
mediaType: 'comic',
|
|
44
|
+
title: volume.name,
|
|
45
|
+
description: volume.deck ?? volume.description ?? undefined,
|
|
46
|
+
creators: creators.length ? creators : undefined,
|
|
47
|
+
publisher: volume.publisher?.name ?? undefined,
|
|
48
|
+
publishedAt: volume.start_year ?? undefined,
|
|
49
|
+
coverUrl: volume.image?.medium_url ?? volume.image?.original_url ?? undefined,
|
|
50
|
+
identifiers: { comicVineId: String(volume.id) },
|
|
51
|
+
sourceUrl: volume.site_detail_url,
|
|
52
|
+
raw: volume,
|
|
53
|
+
lastSyncedAt: new Date().toISOString(),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { MetadataProvider } from '#layers/metadata/shared/types'
|
|
2
|
+
|
|
3
|
+
export const comicVineProvider: MetadataProvider = {
|
|
4
|
+
id: 'comicvine',
|
|
5
|
+
label: 'Comic Vine',
|
|
6
|
+
mediaTypes: ['comic', 'graphic-novel', 'collected-edition'],
|
|
7
|
+
|
|
8
|
+
// fallow-ignore-next-line complexity
|
|
9
|
+
async search({ query, mediaType, limit = 10 }) {
|
|
10
|
+
const resourceType = mediaType === 'collected-edition' || mediaType === 'graphic-novel' ? 'volume' : 'issue'
|
|
11
|
+
|
|
12
|
+
if (resourceType === 'volume') {
|
|
13
|
+
const res = await searchComicVineVolumes(query, limit)
|
|
14
|
+
return (Array.isArray(res.results) ? res.results : []).map(normaliseComicVineVolume)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const [issueRes, volumeRes] = await Promise.all([
|
|
18
|
+
searchComicVineIssues(query, Math.ceil(limit / 2)),
|
|
19
|
+
searchComicVineVolumes(query, Math.floor(limit / 2)),
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
const issues = (Array.isArray(issueRes.results) ? issueRes.results : []).map(normaliseComicVineIssue)
|
|
23
|
+
const volumes = (Array.isArray(volumeRes.results) ? volumeRes.results : []).map(normaliseComicVineVolume)
|
|
24
|
+
return [...issues, ...volumes]
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async lookup({ providerId, resourceType = 'issue' }) {
|
|
28
|
+
if (resourceType === 'volume') {
|
|
29
|
+
const res = await fetchComicVineVolume(providerId)
|
|
30
|
+
return normaliseComicVineVolume(res.results as import('./types').ComicVineVolume)
|
|
31
|
+
}
|
|
32
|
+
const res = await fetchComicVineIssue(providerId)
|
|
33
|
+
return normaliseComicVineIssue(res.results as import('./types').ComicVineIssue)
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async sync({ providerId, resourceType = 'issue' }) {
|
|
37
|
+
return (await comicVineProvider.lookup!({ provider: 'comicvine', providerId, resourceType }))!
|
|
38
|
+
},
|
|
39
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type ComicVineApiResponse<T> = {
|
|
2
|
+
error: string
|
|
3
|
+
limit: number
|
|
4
|
+
offset: number
|
|
5
|
+
number_of_page_results: number
|
|
6
|
+
number_of_total_results: number
|
|
7
|
+
status_code: number
|
|
8
|
+
results: T
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ComicVineIssue = {
|
|
12
|
+
id: number
|
|
13
|
+
name: string | null
|
|
14
|
+
issue_number: string
|
|
15
|
+
description: string | null
|
|
16
|
+
deck: string | null
|
|
17
|
+
cover_date: string | null
|
|
18
|
+
store_date: string | null
|
|
19
|
+
site_detail_url: string
|
|
20
|
+
image: { original_url: string; medium_url: string } | null
|
|
21
|
+
volume: { id: number; name: string } | null
|
|
22
|
+
person_credits: Array<{ id: number; name: string; role: string }> | null
|
|
23
|
+
publisher: { id: number; name: string } | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ComicVineVolume = {
|
|
27
|
+
id: number
|
|
28
|
+
name: string
|
|
29
|
+
description: string | null
|
|
30
|
+
deck: string | null
|
|
31
|
+
start_year: string | null
|
|
32
|
+
site_detail_url: string
|
|
33
|
+
image: { original_url: string; medium_url: string } | null
|
|
34
|
+
publisher: { id: number; name: string } | null
|
|
35
|
+
people: Array<{ id: number; name: string; role: string }> | null
|
|
36
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export default defineNuxtConfig({
|
|
2
|
+
$meta: {
|
|
3
|
+
name: 'metadata-google-books',
|
|
4
|
+
},
|
|
5
|
+
|
|
6
|
+
extends: ['../metadata'],
|
|
7
|
+
|
|
8
|
+
alias: {
|
|
9
|
+
'#layers/metadata-google-books': import.meta.dirname,
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
compatibilityDate: '2026-06-30',
|
|
13
|
+
|
|
14
|
+
runtimeConfig: {
|
|
15
|
+
metadataGoogleBooks: {
|
|
16
|
+
apiKey: '', // env: NUXT_METADATA_GOOGLE_BOOKS_API_KEY (optional — anonymous quota is limited)
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
typescript: {
|
|
21
|
+
typeCheck: false,
|
|
22
|
+
strict: true,
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
declare module '@nuxt/schema' {
|
|
27
|
+
interface RuntimeConfig {
|
|
28
|
+
metadataGoogleBooks: {
|
|
29
|
+
apiKey: string
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kmcom-layer-metadata-google-books",
|
|
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-google-books pnpm -F playground dev",
|
|
9
|
+
"typecheck": "vue-tsc --noEmit -p ../../tsconfig.typecheck.json",
|
|
10
|
+
"lint": "eslint ."
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { GoogleBooksSearchResponse, GoogleBooksVolume } from './types'
|
|
2
|
+
|
|
3
|
+
const BASE = 'https://www.googleapis.com/books/v1'
|
|
4
|
+
|
|
5
|
+
function getApiKey(): string | undefined {
|
|
6
|
+
return useRuntimeConfig().metadataGoogleBooks?.apiKey || undefined
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function searchGoogleBooks(query: string, limit = 10): Promise<GoogleBooksSearchResponse> {
|
|
10
|
+
const params: Record<string, string> = { q: query, maxResults: String(limit) }
|
|
11
|
+
const apiKey = getApiKey()
|
|
12
|
+
if (apiKey) params.key = apiKey
|
|
13
|
+
|
|
14
|
+
return $fetch<GoogleBooksSearchResponse>(`${BASE}/volumes`, { query: params })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function fetchGoogleBooksVolume(id: string): Promise<GoogleBooksVolume> {
|
|
18
|
+
const params: Record<string, string> = {}
|
|
19
|
+
const apiKey = getApiKey()
|
|
20
|
+
if (apiKey) params.key = apiKey
|
|
21
|
+
|
|
22
|
+
return $fetch<GoogleBooksVolume>(`${BASE}/volumes/${id}`, { query: params })
|
|
23
|
+
}
|
|
@@ -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,18 @@
|
|
|
1
|
+
export default defineNuxtConfig({
|
|
2
|
+
$meta: {
|
|
3
|
+
name: 'metadata-openlibrary',
|
|
4
|
+
},
|
|
5
|
+
|
|
6
|
+
extends: ['../metadata'],
|
|
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,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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kmcom-nuxt-layers",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.5.0",
|
|
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-comicvine": "./layers/metadata-comicvine/nuxt.config.ts",
|
|
33
|
+
"./layers/metadata-openlibrary": "./layers/metadata-openlibrary/nuxt.config.ts",
|
|
34
|
+
"./layers/metadata-google-books": "./layers/metadata-google-books/nuxt.config.ts"
|
|
29
35
|
},
|
|
30
36
|
"files": [
|
|
31
37
|
"layers/*/nuxt.config.ts",
|
|
@@ -159,6 +165,7 @@
|
|
|
159
165
|
"@webgpu/glslang": "^0.0.15",
|
|
160
166
|
"@webgpu/types": "^0.1.70",
|
|
161
167
|
"browserslist": "^4.28.2",
|
|
168
|
+
"browserslist-config-baseline": "^0.5.0",
|
|
162
169
|
"changesets": "^1.0.2",
|
|
163
170
|
"csstype": "^3.2.3",
|
|
164
171
|
"cypress": "^15.16.0",
|
|
@@ -207,7 +214,7 @@
|
|
|
207
214
|
"last 2 Firefox major versions",
|
|
208
215
|
"last 2 Safari major versions",
|
|
209
216
|
"last 2 Edge major versions",
|
|
210
|
-
"baseline
|
|
217
|
+
"extends browserslist-config-baseline"
|
|
211
218
|
],
|
|
212
219
|
"dependencies": {
|
|
213
220
|
"node-gyp": "^12.3.0",
|
|
@@ -228,6 +235,8 @@
|
|
|
228
235
|
"dev:ui": "PLAYGROUND_LAYERS=core,ui pnpm -F playground dev",
|
|
229
236
|
"dev:layout": "PLAYGROUND_LAYERS=core,ui,layout pnpm -F playground dev",
|
|
230
237
|
"dev:motion": "PLAYGROUND_LAYERS=core,motion pnpm -F playground dev",
|
|
238
|
+
"dev:database": "PLAYGROUND_LAYERS=core,database pnpm -F playground dev",
|
|
239
|
+
"dev:auth": "PLAYGROUND_LAYERS=core,auth pnpm -F playground dev",
|
|
231
240
|
"build": "turbo run build",
|
|
232
241
|
"lint": "turbo run lint",
|
|
233
242
|
"lint:fix": "turbo run lint -- --fix",
|
package/types/auth.ts
ADDED
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'
|