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
package/README.md CHANGED
@@ -29,6 +29,8 @@ export default defineNuxtConfig({
29
29
  | **motion** | Animations — GSAP, Locomotive Scroll |
30
30
  | **shader** | 3D / WebGPU — Three.js, TresJS |
31
31
  | **forms** | Validation — Zod-based form system |
32
+ | **database** | Storage — Drizzle ORM + Neon Postgres |
33
+ | **auth** | Authentication — nuxt-auth-utils + GitHub OAuth |
32
34
  | **theme** | Theming — color mode, accent colors |
33
35
  | **content** | CMS — @nuxt/content, Nuxt Studio |
34
36
 
@@ -15,6 +15,8 @@ nuxt-layers/
15
15
  │ ├── motion/ # GSAP + Locomotive Scroll animations
16
16
  │ ├── shader/ # Three.js / TresJS WebGPU shaders
17
17
  │ ├── forms/ # Zod-based form validation
18
+ │ ├── database/ # Drizzle + Neon Postgres connection
19
+ │ ├── auth/ # nuxt-auth-utils + GitHub OAuth
18
20
  │ ├── theme/ # Color mode, accent colors, accessibility
19
21
  │ └── content/ # @nuxt/content integration, Nuxt Studio
20
22
  ├── packages/ # Shared utilities (placeholder)
@@ -192,5 +194,7 @@ Some layers (ui, content) support standalone mode via environment variables (`UI
192
194
  | **motion** | Animation layer. GSAP with ScrollTrigger, Locomotive Scroll (Lenis) smooth scrolling, motion composables. Transpiles GSAP for SSR. |
193
195
  | **shader** | WebGPU/WebGL layer. Three.js + TresJS integration, TSL shader support, post-processing. Includes custom shader utilities and auto-imported shader modules. |
194
196
  | **forms** | Form validation layer. Zod schema-based validation, form composables, typed form components. |
197
+ | **database** | Database infrastructure. Drizzle ORM + Neon Postgres via HTTP driver. Exposes `useSql()` and `useDrizzle(schema)` server utilities. App-owned schema and migrations; env: `NUXT_DATABASE_LAYER_URL`. |
198
+ | **auth** | Authentication layer. nuxt-auth-utils + GitHub OAuth handler, sealed-cookie sessions. Opt-in `auth` route middleware; no user table (app's responsibility). Env: `NUXT_SESSION_PASSWORD`, `NUXT_OAUTH_GITHUB_CLIENT_ID/SECRET`. |
195
199
  | **theme** | Theming layer. Color mode switching, accent color palette, theme plugin, CSS custom properties for dynamic theming. |
196
200
  | **content** | Content management layer. @nuxt/content integration, better-sqlite3 for local content DB, optional Nuxt Studio support. |
@@ -0,0 +1,4 @@
1
+ export default defineNuxtRouteMiddleware(() => {
2
+ const { loggedIn } = useUserSession()
3
+ if (!loggedIn.value) return navigateTo('/auth/github')
4
+ })
@@ -0,0 +1,31 @@
1
+ export default defineNuxtConfig({
2
+ $meta: {
3
+ name: 'auth',
4
+ },
5
+
6
+ extends: ['../core'],
7
+
8
+ modules: ['nuxt-auth-utils'],
9
+
10
+ alias: {
11
+ '#layers/auth': import.meta.dirname,
12
+ },
13
+
14
+ compatibilityDate: '2026-06-28',
15
+
16
+ runtimeConfig: {
17
+ // nuxt-auth-utils reads these env vars automatically:
18
+ session: { password: '' }, // env: NUXT_SESSION_PASSWORD
19
+ oauth: {
20
+ github: {
21
+ clientId: '', // env: NUXT_OAUTH_GITHUB_CLIENT_ID
22
+ clientSecret: '', // env: NUXT_OAUTH_GITHUB_CLIENT_SECRET
23
+ },
24
+ },
25
+ },
26
+
27
+ typescript: {
28
+ typeCheck: false,
29
+ strict: true,
30
+ },
31
+ })
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "kmcom-layer-auth",
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,auth pnpm -F playground dev",
9
+ "typecheck": "vue-tsc --noEmit -p ../../tsconfig.typecheck.json",
10
+ "lint": "eslint ."
11
+ },
12
+ "dependencies": {
13
+ "nuxt-auth-utils": "catalog:"
14
+ }
15
+ }
@@ -0,0 +1,13 @@
1
+ export default defineOAuthGitHubEventHandler({
2
+ async onSuccess(event, { user }) {
3
+ await setUserSession(event, {
4
+ user: {
5
+ provider: 'github',
6
+ providerId: user.id,
7
+ username: user.login,
8
+ avatarUrl: user.avatar_url,
9
+ },
10
+ })
11
+ return sendRedirect(event, '/')
12
+ },
13
+ })
@@ -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,17 @@
1
+ <script lang="ts">
2
+ import { defineComponent, h } from 'vue'
3
+
4
+ export default defineComponent({
5
+ name: 'BaselineStatus',
6
+ props: {
7
+ featureId: {
8
+ type: String,
9
+ required: true,
10
+ },
11
+ },
12
+ setup(props) {
13
+ if (import.meta.server) return () => null
14
+ return () => h('baseline-status', { featureId: props.featureId })
15
+ },
16
+ })
17
+ </script>
@@ -0,0 +1,3 @@
1
+ import 'baseline-status'
2
+
3
+ export default defineNuxtPlugin(() => {})
@@ -0,0 +1,24 @@
1
+ export default defineNuxtConfig({
2
+ $meta: {
3
+ name: 'baseline',
4
+ },
5
+
6
+ extends: ['../core'],
7
+
8
+ alias: {
9
+ '#layers/baseline': import.meta.dirname,
10
+ },
11
+
12
+ compatibilityDate: '2026-06-06',
13
+
14
+ vue: {
15
+ compilerOptions: {
16
+ isCustomElement: (tag) => tag === 'baseline-status' || tag === 'baseline-icon',
17
+ },
18
+ },
19
+
20
+ typescript: {
21
+ typeCheck: false,
22
+ strict: true,
23
+ },
24
+ })
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "kmcom-layer-baseline",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "./nuxt.config.ts",
6
+ "scripts": {
7
+ "dev:playground": "PLAYGROUND_LAYERS=core,baseline pnpm -F playground dev",
8
+ "typecheck": "vue-tsc --noEmit -p ../../tsconfig.typecheck.json",
9
+ "lint": "eslint ."
10
+ },
11
+ "dependencies": {
12
+ "baseline-status": "catalog:"
13
+ }
14
+ }
@@ -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,30 @@
1
+ export default defineNuxtConfig({
2
+ $meta: {
3
+ name: 'database',
4
+ },
5
+
6
+ extends: ['../core'],
7
+
8
+ alias: {
9
+ '#layers/database': import.meta.dirname,
10
+ },
11
+
12
+ compatibilityDate: '2026-06-28',
13
+
14
+ runtimeConfig: {
15
+ databaseLayer: {
16
+ url: '', // env: NUXT_DATABASE_LAYER_URL (Neon connection string)
17
+ },
18
+ },
19
+
20
+ typescript: {
21
+ typeCheck: false,
22
+ strict: true,
23
+ },
24
+ })
25
+
26
+ declare module '@nuxt/schema' {
27
+ interface RuntimeConfig {
28
+ databaseLayer?: { url: string }
29
+ }
30
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "kmcom-layer-database",
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,database pnpm -F playground dev",
9
+ "typecheck": "vue-tsc --noEmit -p ../../tsconfig.typecheck.json",
10
+ "lint": "eslint ."
11
+ },
12
+ "dependencies": {
13
+ "@neondatabase/serverless": "catalog:",
14
+ "drizzle-orm": "catalog:"
15
+ }
16
+ }
@@ -0,0 +1,5 @@
1
+ import { drizzle } from 'drizzle-orm/neon-http'
2
+
3
+ export function useDrizzle<TSchema extends Record<string, unknown>>(schema: TSchema) {
4
+ return drizzle(useSql(), { schema })
5
+ }
@@ -0,0 +1,7 @@
1
+ import { neon } from '@neondatabase/serverless'
2
+
3
+ export function useSql() {
4
+ const { databaseLayer } = useRuntimeConfig()
5
+ if (!databaseLayer?.url) throw createError('Database URL not configured')
6
+ return neon(databaseLayer.url)
7
+ }
@@ -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,11 @@
1
+ import type { MetadataProviderId, MetadataRecord } from '#layers/metadata/shared/types'
2
+
3
+ export function useMetadataItem(provider: MetadataProviderId, providerId: string, resourceType?: string) {
4
+ return useAsyncData(
5
+ `metadata-item:${provider}:${providerId}`,
6
+ () =>
7
+ $fetch('/api/metadata/lookup', {
8
+ query: { provider, providerId, resourceType },
9
+ }) as Promise<MetadataRecord>,
10
+ )
11
+ }
@@ -0,0 +1,25 @@
1
+ import type { MetadataMediaType, MetadataProviderId, MetadataSearchResult } from '#layers/metadata/shared/types'
2
+
3
+ type UseMetadataSearchOptions = {
4
+ mediaType?: MetadataMediaType
5
+ providers?: MetadataProviderId[]
6
+ limit?: number
7
+ }
8
+
9
+ export function useMetadataSearch(query: MaybeRef<string>, options: UseMetadataSearchOptions = {}) {
10
+ const q = computed(() => toValue(query).trim())
11
+
12
+ return useAsyncData(
13
+ () => `metadata-search:${q.value}`,
14
+ () =>
15
+ $fetch('/api/metadata/search', {
16
+ query: {
17
+ q: q.value,
18
+ mediaType: options.mediaType,
19
+ providers: options.providers?.join(','),
20
+ limit: options.limit,
21
+ },
22
+ }) as Promise<MetadataSearchResult[]>,
23
+ { watch: [q], immediate: false },
24
+ )
25
+ }
@@ -0,0 +1,27 @@
1
+ export default defineNuxtConfig({
2
+ $meta: {
3
+ name: 'metadata',
4
+ },
5
+
6
+ extends: ['../core'],
7
+
8
+ alias: {
9
+ '#layers/metadata': import.meta.dirname,
10
+ '#layers/metadata/shared': `${import.meta.dirname}/shared`,
11
+ },
12
+
13
+ compatibilityDate: '2026-06-30',
14
+
15
+ nitro: {
16
+ storage: {
17
+ metadata: {
18
+ driver: 'memory',
19
+ },
20
+ },
21
+ },
22
+
23
+ typescript: {
24
+ typeCheck: false,
25
+ strict: true,
26
+ },
27
+ })
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "kmcom-layer-metadata",
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 pnpm -F playground dev",
9
+ "typecheck": "vue-tsc --noEmit -p ../../tsconfig.typecheck.json",
10
+ "lint": "eslint ."
11
+ }
12
+ }
@@ -0,0 +1,36 @@
1
+ export default defineNuxtConfig({
2
+ $meta: {
3
+ name: 'metadata-comicvine',
4
+ },
5
+
6
+ extends: ['../../'],
7
+
8
+ alias: {
9
+ '#layers/metadata-comicvine': import.meta.dirname,
10
+ },
11
+
12
+ compatibilityDate: '2026-06-30',
13
+
14
+ runtimeConfig: {
15
+ metadataComicvine: {
16
+ apiKey: '', // env: NUXT_METADATA_COMICVINE_API_KEY
17
+ baseUrl: 'https://comicvine.gamespot.com/api',
18
+ cacheTtl: 60 * 60 * 24 * 30,
19
+ },
20
+ },
21
+
22
+ typescript: {
23
+ typeCheck: false,
24
+ strict: true,
25
+ },
26
+ })
27
+
28
+ declare module '@nuxt/schema' {
29
+ interface RuntimeConfig {
30
+ metadataComicvine: {
31
+ apiKey: string
32
+ baseUrl: string
33
+ cacheTtl: number
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "kmcom-layer-metadata-comicvine",
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-comicvine 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(comicVineProvider)
3
+ })
@@ -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,7 @@
1
+ {
2
+ "extends": "../../../apps/playground/tsconfig.json",
3
+ "compilerOptions": {
4
+ "strict": true
5
+ },
6
+ "exclude": ["node_modules"]
7
+ }
@@ -0,0 +1,32 @@
1
+ export default defineNuxtConfig({
2
+ $meta: {
3
+ name: 'metadata-google-books',
4
+ },
5
+
6
+ extends: ['../../'],
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,3 @@
1
+ export default defineNitroPlugin(() => {
2
+ registerProvider(googleBooksProvider)
3
+ })
@@ -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
+ }