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.
- 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/providers/comicvine/nuxt.config.ts +36 -0
- package/layers/metadata/providers/comicvine/package.json +12 -0
- package/layers/metadata/providers/comicvine/server/plugins/comicvine-register.ts +3 -0
- package/layers/metadata/providers/comicvine/server/utils/metadata-comicvine/client.ts +52 -0
- package/layers/metadata/providers/comicvine/server/utils/metadata-comicvine/normalise.ts +55 -0
- package/layers/metadata/providers/comicvine/server/utils/metadata-comicvine/provider.ts +39 -0
- package/layers/metadata/providers/comicvine/server/utils/metadata-comicvine/types.ts +36 -0
- package/layers/metadata/providers/comicvine/tsconfig.json +7 -0
- package/layers/metadata/providers/google-books/nuxt.config.ts +32 -0
- package/layers/metadata/providers/google-books/package.json +12 -0
- package/layers/metadata/providers/google-books/server/plugins/google-books-register.ts +3 -0
- package/layers/metadata/providers/google-books/server/utils/metadata-google-books/client.ts +23 -0
- package/layers/metadata/providers/google-books/server/utils/metadata-google-books/normalise.ts +39 -0
- package/layers/metadata/providers/google-books/server/utils/metadata-google-books/provider.ts +21 -0
- package/layers/metadata/providers/google-books/server/utils/metadata-google-books/types.ts +25 -0
- package/layers/metadata/providers/google-books/tsconfig.json +7 -0
- package/layers/metadata/providers/openlibrary/nuxt.config.ts +18 -0
- package/layers/metadata/providers/openlibrary/package.json +12 -0
- package/layers/metadata/providers/openlibrary/server/plugins/openlibrary-register.ts +3 -0
- package/layers/metadata/providers/openlibrary/server/utils/metadata-openlibrary/client.ts +30 -0
- package/layers/metadata/providers/openlibrary/server/utils/metadata-openlibrary/normalise.ts +81 -0
- package/layers/metadata/providers/openlibrary/server/utils/metadata-openlibrary/provider.ts +25 -0
- package/layers/metadata/providers/openlibrary/server/utils/metadata-openlibrary/types.ts +48 -0
- package/layers/metadata/providers/openlibrary/tsconfig.json +7 -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/package.json +16 -3
- package/types/auth.ts +7 -0
- 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
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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,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,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,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,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,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,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: ['../../'],
|
|
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
|
+
}
|