mikser-io-sdk-api 3.0.0 → 3.1.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/index.d.ts CHANGED
@@ -187,3 +187,86 @@ export declare class MikserError extends Error {
187
187
  status: number
188
188
  body: { error?: string } | undefined
189
189
  }
190
+
191
+ // ────────────────────────────────────────────────────────────────────
192
+ // Pure utilities — framework SDKs wrap these in their own reactivity.
193
+ // ────────────────────────────────────────────────────────────────────
194
+
195
+ export interface GenerateRoutesOptions<TRoute = unknown> {
196
+ /** A mikser entities client (the result of createClient(...).entities(name)). */
197
+ client: { listAll(query: ListQuery): Promise<Array<{ id: string; meta?: Record<string, unknown> }>> }
198
+ /** Sift filter — defaults to "published documents that declare meta.route". */
199
+ filter?: Filter
200
+ /** Maps a catalog document to a route descriptor (or null to skip). */
201
+ mapRoute: (document: { id: string; meta?: Record<string, unknown> }) => TRoute | null
202
+ }
203
+
204
+ /**
205
+ * Build-time route enumeration. Auto-paginates via listAll() and
206
+ * applies `mapRoute` to every catalog entity that matches the filter.
207
+ * `null` returns from `mapRoute` are dropped.
208
+ */
209
+ export function generateMikserRoutes<TRoute = unknown>(
210
+ options: GenerateRoutesOptions<TRoute>,
211
+ ): Promise<TRoute[]>
212
+
213
+ export interface HrefIndexOptions {
214
+ /** Fallback language tag for documents that don't declare meta.lang. Default 'default'. */
215
+ defaultLang?: string
216
+ }
217
+
218
+ export interface HrefIndex {
219
+ /** Resolve a logical reference (`/about`) to a deployed URL for the given language. */
220
+ href(ref: string, lang?: string): string
221
+ /** Reverse — given a deployed URL, return the logical reference it belongs to. */
222
+ refFor(url: string | null): string | null
223
+ /** Alternates for a deployed URL — `current` plus the alternate-language URLs. */
224
+ alternates(options: { route: string | null; languages?: string[] }): {
225
+ current: { lang: string | null; url: string; ref: string } | null
226
+ alternates: Array<{ lang: string; url: string }>
227
+ }
228
+ /** Raw `ref → { lang → url }` map, for inspection / debugging. */
229
+ map: Record<string, Record<string, string>>
230
+ }
231
+
232
+ /**
233
+ * Build a multilingual href lookup from a snapshot of catalog documents.
234
+ * Pure data transformation — wrap in a framework-specific reactive
235
+ * shell to drive `useHref` / `useAlternates` composables.
236
+ */
237
+ export function createHrefIndex(
238
+ documents: Array<{ meta?: Record<string, unknown> }>,
239
+ options?: HrefIndexOptions,
240
+ ): HrefIndex
241
+
242
+ export interface AssetRecord {
243
+ url: string
244
+ width?: number
245
+ height?: number
246
+ srcset?: string
247
+ alt?: string
248
+ meta?: Record<string, unknown>
249
+ }
250
+
251
+ export interface ImageProps {
252
+ src: string
253
+ width?: number
254
+ height?: number
255
+ srcset?: string
256
+ alt?: string
257
+ }
258
+
259
+ export interface AssetIndex {
260
+ asset(ref: string): AssetRecord | null
261
+ image(ref: string): ImageProps | null
262
+ map: Record<string, AssetRecord>
263
+ }
264
+
265
+ /**
266
+ * Build an asset metadata lookup from a snapshot of asset entities.
267
+ * Pure data transformation — wrap in a framework-specific reactive
268
+ * shell to drive `useAsset` composables.
269
+ */
270
+ export function createAssetIndex(
271
+ assets: Array<{ id: string; meta?: Record<string, unknown> }>,
272
+ ): AssetIndex
package/index.js CHANGED
@@ -27,6 +27,9 @@
27
27
  // src/url.js — URL building (joinUrl, sortToParam, filterToParams)
28
28
  // src/sse.js — SSE event parser
29
29
  // src/entities.js — per-endpoint entities client (list / watch / live / ...)
30
+ // src/routes.js — generateMikserRoutes (build-time route enumeration)
31
+ // src/href.js — createHrefIndex (multilingual reference → URL lookup)
32
+ // src/asset.js — createAssetIndex (asset metadata lookup)
30
33
  import { MikserError } from './src/error.js'
31
34
  import { createEntitiesClient } from './src/entities.js'
32
35
 
@@ -60,3 +63,6 @@ export function createClient({
60
63
  }
61
64
 
62
65
  export { MikserError }
66
+ export { generateMikserRoutes } from './src/routes.js'
67
+ export { createHrefIndex } from './src/href.js'
68
+ export { createAssetIndex } from './src/asset.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io-sdk-api",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Client SDK for mikser-io's api plugin — query the document catalog from the browser or Node",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
package/src/asset.js ADDED
@@ -0,0 +1,65 @@
1
+ // Asset metadata index — pure data version. Framework SDKs wrap this
2
+ // in their own reactivity primitives and expose useAsset on top.
3
+ //
4
+ // When assets carry metadata the template needs (dimensions, srcset,
5
+ // alt text), looking them up by reference is cleaner than re-fetching
6
+ // per render. The convention is that asset entities have an `id`
7
+ // (used as the reference key) plus a `meta` block with the metadata.
8
+
9
+ /**
10
+ * @param {Array<{id: string, meta?: object}>} assets Asset entities.
11
+ * @returns {{
12
+ * asset: (ref: string) => AssetRecord|null,
13
+ * image: (ref: string) => ImageProps|null,
14
+ * map: Record<string, AssetRecord>,
15
+ * }}
16
+ *
17
+ * @typedef {Object} AssetRecord
18
+ * @property {string} url
19
+ * @property {number|undefined} width
20
+ * @property {number|undefined} height
21
+ * @property {string|undefined} srcset
22
+ * @property {string|undefined} alt
23
+ * @property {object|undefined} meta The raw meta block, for downstream use.
24
+ *
25
+ * @typedef {Object} ImageProps
26
+ * @property {string} src
27
+ * @property {number|undefined} width
28
+ * @property {number|undefined} height
29
+ * @property {string|undefined} srcset
30
+ * @property {string|undefined} alt
31
+ */
32
+ export function createAssetIndex(assets) {
33
+ const map = {}
34
+ if (Array.isArray(assets)) {
35
+ for (const a of assets) {
36
+ if (!a?.id) continue
37
+ map[a.id] = {
38
+ url: a.meta?.destination ?? a.meta?.url ?? a.id,
39
+ width: a.meta?.width,
40
+ height: a.meta?.height,
41
+ srcset: a.meta?.srcset,
42
+ alt: a.meta?.alt,
43
+ meta: a.meta,
44
+ }
45
+ }
46
+ }
47
+
48
+ function asset(ref) {
49
+ return map[ref] ?? null
50
+ }
51
+
52
+ function image(ref) {
53
+ const a = map[ref]
54
+ if (!a) return null
55
+ return {
56
+ src: a.url,
57
+ width: a.width,
58
+ height: a.height,
59
+ srcset: a.srcset,
60
+ alt: a.alt,
61
+ }
62
+ }
63
+
64
+ return { asset, image, map }
65
+ }
package/src/href.js ADDED
@@ -0,0 +1,110 @@
1
+ // Multilingual href resolution — pure data version. Framework SDKs
2
+ // (Vue/React/Svelte) wrap this in their own reactivity primitives and
3
+ // expose useHref / useAlternates composables on top.
4
+ //
5
+ // The "logical reference → deployed URL" mapping lets the consumer
6
+ // link to a content-shaped identifier (`/about`) and have it resolve
7
+ // to whatever URL the current locale serves it at (`/en/about`,
8
+ // `/fr/a-propos`, etc.). Convention is three meta fields:
9
+ //
10
+ // meta.href: '/about' (logical reference)
11
+ // meta.lang: 'en' (language this doc represents)
12
+ // meta.route: '/en/about' (deployed URL)
13
+ //
14
+ // Anything missing `meta.href` is excluded from the index.
15
+
16
+ /**
17
+ * @param {Array<{meta?: object}>} documents Catalog documents with meta.
18
+ * @param {Object} [options]
19
+ * @param {string} [options.defaultLang='default'] Fallback language tag
20
+ * for documents that don't declare meta.lang. The literal string
21
+ * `'default'` is treated as a fallback bucket in lookups.
22
+ * @returns {{
23
+ * href: (ref: string, lang?: string) => string,
24
+ * refFor: (url: string|null) => string|null,
25
+ * alternates: (opts: { route: string|null, languages?: string[] }) => { current: {lang, url, ref}|null, alternates: Array<{lang, url}> },
26
+ * map: Record<string, Record<string, string>>,
27
+ * }}
28
+ */
29
+ export function createHrefIndex(documents, { defaultLang = 'default' } = {}) {
30
+ const map = {}
31
+ if (Array.isArray(documents)) {
32
+ for (const document of documents) {
33
+ const ref = document?.meta?.href
34
+ if (!ref) continue
35
+ const lang = document.meta?.lang ?? defaultLang
36
+ const url = document.meta?.route ?? document.meta?.destination ?? ref
37
+ if (!map[ref]) map[ref] = {}
38
+ map[ref][lang] = url
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Resolve a logical reference to a deployed URL.
44
+ *
45
+ * Fallback chain: requested lang → `'default'` bucket → any
46
+ * available language → the input reference unchanged (so broken
47
+ * references stay visible rather than silently becoming undefined).
48
+ */
49
+ function href(ref, lang) {
50
+ const target = lang ?? defaultLang
51
+ const entry = map[ref]
52
+ if (!entry) return ref
53
+ return entry[target]
54
+ ?? entry['default']
55
+ ?? Object.values(entry)[0]
56
+ ?? ref
57
+ }
58
+
59
+ /**
60
+ * Reverse lookup — given a deployed URL, return the logical
61
+ * reference it belongs to (or null if it's not in the index).
62
+ */
63
+ function refFor(url) {
64
+ if (url == null) return null
65
+ for (const [ref, byLang] of Object.entries(map)) {
66
+ if (Object.values(byLang).includes(url)) return ref
67
+ }
68
+ return null
69
+ }
70
+
71
+ /**
72
+ * Alternates for a deployed URL — useful for hreflang tags and
73
+ * language switchers.
74
+ *
75
+ * `languages` controls the alternate set:
76
+ * - omitted: only return languages that actually exist in the
77
+ * catalog for this ref. Right shape for hreflang (don't
78
+ * advertise translations that don't exist).
79
+ * - provided as an array: return one entry per requested
80
+ * language, using href()'s fallback chain when a translation
81
+ * doesn't exist. Right shape for language switchers (show
82
+ * every locale the app supports).
83
+ *
84
+ * The current page's own language is excluded from `alternates` —
85
+ * it's what `current` is for. Callers that want it included can
86
+ * prepend `current` themselves.
87
+ */
88
+ function alternates({ route, languages } = {}) {
89
+ if (route == null) return { current: null, alternates: [] }
90
+ const ref = refFor(route)
91
+ if (ref == null) return { current: null, alternates: [] }
92
+ const entry = map[ref] ?? {}
93
+ const currentLang = Object.entries(entry).find(([, url]) => url === route)?.[0] ?? null
94
+ const current = { lang: currentLang, url: route, ref }
95
+
96
+ let list
97
+ if (Array.isArray(languages)) {
98
+ list = languages
99
+ .filter(lang => lang !== currentLang)
100
+ .map(lang => ({ lang, url: href(ref, lang) }))
101
+ } else {
102
+ list = Object.entries(entry)
103
+ .filter(([lang]) => lang !== currentLang && lang !== 'default')
104
+ .map(([lang, url]) => ({ lang, url }))
105
+ }
106
+ return { current, alternates: list }
107
+ }
108
+
109
+ return { href, refFor, alternates, map }
110
+ }
package/src/routes.js ADDED
@@ -0,0 +1,34 @@
1
+ // Build-time route enumeration. Given a mikser entities client and a
2
+ // mapRoute function, return the array of route descriptors produced by
3
+ // applying `mapRoute` to every catalog entity that matches the filter.
4
+ //
5
+ // Auto-paginates via the client's listAll() under the hood — no manual
6
+ // limit, no silent truncation on large catalogs.
7
+ //
8
+ // Framework-agnostic: the `mapRoute` return shape is whatever your build
9
+ // pipeline expects (vite-ssg routes, Next pages, SvelteKit entries, etc.).
10
+ // Framework SDKs re-export this with their own typed mapRoute signatures.
11
+
12
+ const DEFAULT_FILTER = { 'meta.published': true, 'meta.route': { $exists: true } }
13
+
14
+ /**
15
+ * @param {Object} options
16
+ * @param {Object} options.client A mikser entities client (the result of
17
+ * createClient(...).entities(name)).
18
+ * @param {Object} [options.filter] Sift filter — defaults to "published
19
+ * documents that declare meta.route".
20
+ * @param {Function} options.mapRoute (document) => routeDescriptor | null.
21
+ * Null returns are dropped.
22
+ * @returns {Promise<Array>} The mapped route descriptors.
23
+ */
24
+ export async function generateMikserRoutes({
25
+ client,
26
+ filter = DEFAULT_FILTER,
27
+ mapRoute,
28
+ } = {}) {
29
+ if (!client) throw new Error('generateMikserRoutes: { client } is required')
30
+ if (!mapRoute) throw new Error('generateMikserRoutes: { mapRoute } is required')
31
+
32
+ const items = await client.listAll({ filter, fields: ['id', 'meta'] })
33
+ return items.map(mapRoute).filter(r => r != null)
34
+ }