@contentrain/query 5.1.5 → 6.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.
Files changed (36) hide show
  1. package/README.md +13 -0
  2. package/dist/cdn/index.cjs +1 -1
  3. package/dist/cdn/index.d.cts +1 -1
  4. package/dist/cdn/index.d.mts +1 -1
  5. package/dist/cdn/index.mjs +1 -1
  6. package/dist/{cdn-SOuikUcY.mjs → cdn-CNdH7sKk.mjs} +23 -2
  7. package/dist/cdn-CNdH7sKk.mjs.map +1 -0
  8. package/dist/{cdn-5ycdk2ET.cjs → cdn-aeCpH9Dt.cjs} +22 -1
  9. package/dist/cli.cjs +1 -1
  10. package/dist/cli.mjs +1 -1
  11. package/dist/{generate-DAaCl3Np.cjs → generate-DN3mAbvC.cjs} +108 -44
  12. package/dist/{generate-B5P14n43.mjs → generate-mpKW9Y8v.mjs} +109 -45
  13. package/dist/generate-mpKW9Y8v.mjs.map +1 -0
  14. package/dist/generator/generate.cjs +1 -1
  15. package/dist/generator/generate.d.cts +8 -0
  16. package/dist/generator/generate.d.cts.map +1 -1
  17. package/dist/generator/generate.d.mts +8 -0
  18. package/dist/generator/generate.d.mts.map +1 -1
  19. package/dist/generator/generate.mjs +1 -1
  20. package/dist/{index-BChs94uA.d.cts → index-mBPlkuGU.d.cts} +4 -1
  21. package/dist/index-mBPlkuGU.d.cts.map +1 -0
  22. package/dist/{index-CsCpovuB.d.mts → index-tyAxQaPt.d.mts} +4 -1
  23. package/dist/index-tyAxQaPt.d.mts.map +1 -0
  24. package/dist/index.cjs +24 -3
  25. package/dist/index.d.cts +4 -1
  26. package/dist/index.d.cts.map +1 -1
  27. package/dist/index.d.mts +4 -1
  28. package/dist/index.d.mts.map +1 -1
  29. package/dist/index.mjs +24 -3
  30. package/dist/index.mjs.map +1 -1
  31. package/package.json +2 -2
  32. package/skills/contentrain-query/references/bundler-config.md +10 -8
  33. package/dist/cdn-SOuikUcY.mjs.map +0 -1
  34. package/dist/generate-B5P14n43.mjs.map +0 -1
  35. package/dist/index-BChs94uA.d.cts.map +0 -1
  36. package/dist/index-CsCpovuB.d.mts.map +0 -1
package/README.md CHANGED
@@ -159,6 +159,19 @@ Supported methods:
159
159
  - `first()`
160
160
  - `all()`
161
161
 
162
+ ### `media(value)`
163
+
164
+ Resolves a stored `media/...` path to its absolute delivery URL. Emitted **only** when a CDN base is configured — set `cdn.url` in `.contentrain/config.json` or run `contentrain generate --cdnBaseUrl <base>`:
165
+
166
+ ```ts
167
+ import { media } from '#contentrain'
168
+
169
+ media('media/original/hero.webp') // → '{cdn.url}/media/original/hero.webp'
170
+ media('https://images.unsplash.com/x.jpg') // → unchanged (external pass-through)
171
+ ```
172
+
173
+ Idempotent — external URLs (`http(s)://`, `//`, `data:`) and already-absolute delivery URLs pass through untouched. It is the local-mode counterpart of CDN mode's `MediaAccessor.url()`. For Studio-CDN content, media fields already carry absolute URLs, so no resolution is needed.
174
+
162
175
  ## 🔗 Relations
163
176
 
164
177
  Generated clients support relation resolution via `include(...)`.
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_cdn = require("../cdn-5ycdk2ET.cjs");
2
+ const require_cdn = require("../cdn-aeCpH9Dt.cjs");
3
3
  exports.CdnCollectionQuery = require_cdn.CdnCollectionQuery;
4
4
  exports.CdnDictionaryAccessor = require_cdn.CdnDictionaryAccessor;
5
5
  exports.CdnDocumentQuery = require_cdn.CdnDocumentQuery;
@@ -1,2 +1,2 @@
1
- import { C as CdnDocumentQuery, E as CdnCollectionQuery, F as SingletonDataSource, M as CollectionDataSource, N as DictionaryDataSource, P as DocumentDataSource, S as MediaManifest, T as CdnSingletonAccessor, _ as FormsClient, a as ConversationClient, b as MediaAsset, c as ConversationHistory, d as ConversationSendOptions, f as ConversationToolResult, g as FormSubmitResult, h as FormFieldConfig, i as ContentrainError, j as HttpTransport, l as ConversationMessage, m as FormConfig, n as ContentrainCDNConfig, o as ConversationClientConfig, p as ConversationUsage, r as createContentrain, s as ConversationContext, t as ContentrainCDNClient, u as ConversationResponse, v as FormsClientConfig, w as CdnDictionaryAccessor, x as MediaAssetMeta, y as MediaAccessor } from "../index-BChs94uA.cjs";
1
+ import { C as CdnDocumentQuery, E as CdnCollectionQuery, F as SingletonDataSource, M as CollectionDataSource, N as DictionaryDataSource, P as DocumentDataSource, S as MediaManifest, T as CdnSingletonAccessor, _ as FormsClient, a as ConversationClient, b as MediaAsset, c as ConversationHistory, d as ConversationSendOptions, f as ConversationToolResult, g as FormSubmitResult, h as FormFieldConfig, i as ContentrainError, j as HttpTransport, l as ConversationMessage, m as FormConfig, n as ContentrainCDNConfig, o as ConversationClientConfig, p as ConversationUsage, r as createContentrain, s as ConversationContext, t as ContentrainCDNClient, u as ConversationResponse, v as FormsClientConfig, w as CdnDictionaryAccessor, x as MediaAssetMeta, y as MediaAccessor } from "../index-mBPlkuGU.cjs";
2
2
  export { CdnCollectionQuery, CdnDictionaryAccessor, CdnDocumentQuery, CdnSingletonAccessor, CollectionDataSource, ContentrainCDNClient, ContentrainCDNConfig, ContentrainError, ConversationClient, ConversationClientConfig, ConversationContext, ConversationHistory, ConversationMessage, ConversationResponse, ConversationSendOptions, ConversationToolResult, ConversationUsage, DictionaryDataSource, DocumentDataSource, FormConfig, FormFieldConfig, FormSubmitResult, FormsClient, FormsClientConfig, HttpTransport, MediaAccessor, MediaAsset, MediaAssetMeta, MediaManifest, SingletonDataSource, createContentrain };
@@ -1,2 +1,2 @@
1
- import { C as CdnDocumentQuery, E as CdnCollectionQuery, F as SingletonDataSource, M as CollectionDataSource, N as DictionaryDataSource, P as DocumentDataSource, S as MediaManifest, T as CdnSingletonAccessor, _ as FormsClient, a as ConversationClient, b as MediaAsset, c as ConversationHistory, d as ConversationSendOptions, f as ConversationToolResult, g as FormSubmitResult, h as FormFieldConfig, i as ContentrainError, j as HttpTransport, l as ConversationMessage, m as FormConfig, n as ContentrainCDNConfig, o as ConversationClientConfig, p as ConversationUsage, r as createContentrain, s as ConversationContext, t as ContentrainCDNClient, u as ConversationResponse, v as FormsClientConfig, w as CdnDictionaryAccessor, x as MediaAssetMeta, y as MediaAccessor } from "../index-CsCpovuB.mjs";
1
+ import { C as CdnDocumentQuery, E as CdnCollectionQuery, F as SingletonDataSource, M as CollectionDataSource, N as DictionaryDataSource, P as DocumentDataSource, S as MediaManifest, T as CdnSingletonAccessor, _ as FormsClient, a as ConversationClient, b as MediaAsset, c as ConversationHistory, d as ConversationSendOptions, f as ConversationToolResult, g as FormSubmitResult, h as FormFieldConfig, i as ContentrainError, j as HttpTransport, l as ConversationMessage, m as FormConfig, n as ContentrainCDNConfig, o as ConversationClientConfig, p as ConversationUsage, r as createContentrain, s as ConversationContext, t as ContentrainCDNClient, u as ConversationResponse, v as FormsClientConfig, w as CdnDictionaryAccessor, x as MediaAssetMeta, y as MediaAccessor } from "../index-tyAxQaPt.mjs";
2
2
  export { CdnCollectionQuery, CdnDictionaryAccessor, CdnDocumentQuery, CdnSingletonAccessor, CollectionDataSource, ContentrainCDNClient, ContentrainCDNConfig, ContentrainError, ConversationClient, ConversationClientConfig, ConversationContext, ConversationHistory, ConversationMessage, ConversationResponse, ConversationSendOptions, ConversationToolResult, ConversationUsage, DictionaryDataSource, DocumentDataSource, FormConfig, FormFieldConfig, FormSubmitResult, FormsClient, FormsClientConfig, HttpTransport, MediaAccessor, MediaAsset, MediaAssetMeta, MediaManifest, SingletonDataSource, createContentrain };
@@ -1,2 +1,2 @@
1
- import { a as CdnDocumentQuery, c as CdnCollectionQuery, i as MediaAccessor, l as HttpTransport, n as ConversationClient, o as CdnDictionaryAccessor, r as FormsClient, s as CdnSingletonAccessor, t as createContentrain, u as ContentrainError } from "../cdn-SOuikUcY.mjs";
1
+ import { a as CdnDocumentQuery, c as CdnCollectionQuery, i as MediaAccessor, l as HttpTransport, n as ConversationClient, o as CdnDictionaryAccessor, r as FormsClient, s as CdnSingletonAccessor, t as createContentrain, u as ContentrainError } from "../cdn-CNdH7sKk.mjs";
2
2
  export { CdnCollectionQuery, CdnDictionaryAccessor, CdnDocumentQuery, CdnSingletonAccessor, ContentrainError, ConversationClient, FormsClient, HttpTransport, MediaAccessor, createContentrain };
@@ -5,7 +5,9 @@ function applyWhere(item, clause) {
5
5
  case "eq":
6
6
  if (Array.isArray(val)) return val.includes(clause.value);
7
7
  return val === clause.value;
8
- case "ne": return val !== clause.value;
8
+ case "ne":
9
+ if (Array.isArray(val)) return !val.includes(clause.value);
10
+ return val !== clause.value;
9
11
  case "gt": return val > clause.value;
10
12
  case "gte": return val >= clause.value;
11
13
  case "lt": return val < clause.value;
@@ -275,6 +277,8 @@ var CdnDocumentQuery = class {
275
277
  _source;
276
278
  _locale = "en";
277
279
  _filters = [];
280
+ _sortField = null;
281
+ _sortOrder = "asc";
278
282
  constructor(source, defaultLocale) {
279
283
  this._source = source;
280
284
  if (defaultLocale) this._locale = defaultLocale;
@@ -291,9 +295,26 @@ var CdnDocumentQuery = class {
291
295
  });
292
296
  return this;
293
297
  }
298
+ sort(field, order = "asc") {
299
+ this._sortField = field;
300
+ this._sortOrder = order;
301
+ return this;
302
+ }
294
303
  async all() {
295
304
  let items = await this._source.getIndex(this._locale);
296
305
  for (const clause of this._filters) items = items.filter((item) => applyWhere(item, clause));
306
+ if (this._sortField) {
307
+ const sf = this._sortField;
308
+ const dir = this._sortOrder === "asc" ? 1 : -1;
309
+ items = items.toSorted((a, b) => {
310
+ const va = a[sf];
311
+ const vb = b[sf];
312
+ if (va == null && vb == null) return 0;
313
+ if (va == null) return dir;
314
+ if (vb == null) return -dir;
315
+ return va < vb ? -dir : va > vb ? dir : 0;
316
+ });
317
+ }
297
318
  return items;
298
319
  }
299
320
  async count() {
@@ -441,4 +462,4 @@ function createContentrain(config) {
441
462
  //#endregion
442
463
  export { CdnDocumentQuery as a, CdnCollectionQuery as c, applyWhere as d, MediaAccessor as i, HttpTransport as l, ConversationClient as n, CdnDictionaryAccessor as o, FormsClient as r, CdnSingletonAccessor as s, createContentrain as t, ContentrainError as u };
443
464
 
444
- //# sourceMappingURL=cdn-SOuikUcY.mjs.map
465
+ //# sourceMappingURL=cdn-CNdH7sKk.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdn-CNdH7sKk.mjs","names":[],"sources":["../src/shared/where.ts","../src/cdn/errors.ts","../src/cdn/http-transport.ts","../src/cdn/collection-query.ts","../src/cdn/singleton-accessor.ts","../src/cdn/dictionary-accessor.ts","../src/cdn/document-query.ts","../src/cdn/media-accessor.ts","../src/cdn/forms-client.ts","../src/cdn/conversation-client.ts","../src/cdn/index.ts"],"sourcesContent":["export type WhereOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'contains'\n\nexport interface WhereClause {\n field: string\n op: WhereOp\n value: unknown\n}\n\nexport function applyWhere<T>(item: T, clause: WhereClause): boolean {\n const val = (item as Record<string, unknown>)[clause.field]\n switch (clause.op) {\n case 'eq': {\n if (Array.isArray(val)) return val.includes(clause.value)\n return val === clause.value\n }\n case 'ne': {\n if (Array.isArray(val)) return !val.includes(clause.value)\n return val !== clause.value\n }\n case 'gt': return (val as number) > (clause.value as number)\n case 'gte': return (val as number) >= (clause.value as number)\n case 'lt': return (val as number) < (clause.value as number)\n case 'lte': return (val as number) <= (clause.value as number)\n case 'in': return Array.isArray(clause.value) && (clause.value as unknown[]).includes(val)\n case 'contains': {\n if (typeof val === 'string') return val.includes(clause.value as string)\n if (Array.isArray(val)) return val.includes(clause.value)\n return false\n }\n default: return true\n }\n}\n","export class ContentrainError extends Error {\n constructor(\n public status: number,\n message: string,\n ) {\n super(message)\n this.name = 'ContentrainError'\n }\n}\n","import { ContentrainError } from './errors.js'\nimport type { CollectionDataSource, SingletonDataSource, DictionaryDataSource, DocumentDataSource } from './data-source.js'\n\ninterface CacheEntry {\n data: unknown\n etag: string\n}\n\nexport class HttpTransport {\n private _baseUrl: string\n private _projectId: string\n private _apiKey: string\n private _cache = new Map<string, CacheEntry>()\n\n constructor(config: { baseUrl: string; projectId: string; apiKey: string }) {\n this._baseUrl = config.baseUrl.replace(/\\/+$/, '')\n this._projectId = config.projectId\n this._apiKey = config.apiKey\n }\n\n buildUrl(path: string): string {\n return `${this._baseUrl}/${this._projectId}/${path}`\n }\n\n async fetch<T>(path: string): Promise<T> {\n const url = `${this._baseUrl}/${this._projectId}/${path}`\n const cached = this._cache.get(path)\n\n const headers: Record<string, string> = {\n 'Authorization': `Bearer ${this._apiKey}`,\n }\n if (cached?.etag) {\n headers['If-None-Match'] = cached.etag\n }\n\n const res = await globalThis.fetch(url, { headers })\n\n if (res.status === 304 && cached) return cached.data as T\n if (!res.ok) throw new ContentrainError(res.status, await res.text())\n\n const data = (await res.json()) as T\n const etag = res.headers.get('etag') ?? ''\n if (etag) {\n this._cache.set(path, { data, etag })\n }\n return data\n }\n\n collection<T>(modelId: string): CollectionDataSource<T> {\n return {\n getAll: async (locale) => {\n const map = await this.fetch<Record<string, T>>(`content/${modelId}/${locale}.json`)\n return Object.entries(map).map(([id, entry]) => Object.assign({ id }, entry as object) as T)\n },\n getOne: async (id, locale) => {\n const map = await this.fetch<Record<string, T>>(`content/${modelId}/${locale}.json`)\n const entry = map[id]\n return entry ? { id, ...entry as object } as T : null\n },\n }\n }\n\n singleton<T>(modelId: string): SingletonDataSource<T> {\n return {\n get: (locale) => this.fetch<T>(`content/${modelId}/${locale}.json`),\n }\n }\n\n dictionary(modelId: string): DictionaryDataSource {\n return {\n get: (locale) => this.fetch<Record<string, string>>(`content/${modelId}/${locale}.json`),\n }\n }\n\n document<T>(modelId: string): DocumentDataSource<T> {\n return {\n getIndex: (locale) => this.fetch<T[]>(`documents/${modelId}/_index/${locale}.json`),\n getBySlug: (slug, locale) => this.fetch(`documents/${modelId}/${slug}/${locale}.json`),\n }\n }\n}\n","import type { CollectionDataSource } from './data-source.js'\nimport type { HttpTransport } from './http-transport.js'\nimport type { WhereOp, WhereClause } from '../shared/where.js'\nimport { applyWhere } from '../shared/where.js'\n\nexport interface EntryMeta {\n status?: string\n publish_at?: string\n expire_at?: string\n updated_by?: string\n approved_by?: string\n [key: string]: unknown\n}\n\nexport class CdnCollectionQuery<T extends object> {\n private _transport: HttpTransport\n private _source: CollectionDataSource<T>\n private _modelId: string\n private _locale: string = 'en'\n private _filters: WhereClause[] = []\n private _sortField: string | null = null\n private _sortOrder: 'asc' | 'desc' = 'asc'\n private _limit: number | null = null\n private _offset = 0\n private _includes: string[] = []\n private _withMeta = false\n\n constructor(transport: HttpTransport, modelId: string, defaultLocale?: string) {\n this._transport = transport\n this._source = transport.collection<T>(modelId)\n this._modelId = modelId\n if (defaultLocale) this._locale = defaultLocale\n }\n\n locale(lang: string): this {\n this._locale = lang\n return this\n }\n\n where(field: string, op: WhereOp, value: unknown): this {\n this._filters.push({ field, op, value })\n return this\n }\n\n sort(field: string, order: 'asc' | 'desc' = 'asc'): this {\n this._sortField = field\n this._sortOrder = order\n return this\n }\n\n limit(n: number): this {\n this._limit = n\n return this\n }\n\n offset(n: number): this {\n this._offset = n\n return this\n }\n\n include(...fields: string[]): this {\n this._includes.push(...fields)\n return this\n }\n\n withMeta(): this {\n this._withMeta = true\n return this\n }\n\n async all(): Promise<T[]> {\n let items = await this._source.getAll(this._locale)\n\n // Filter\n for (const clause of this._filters) {\n items = items.filter(item => applyWhere(item, clause))\n }\n\n // Sort\n if (this._sortField) {\n const field = this._sortField\n const dir = this._sortOrder === 'asc' ? 1 : -1\n items = items.toSorted((a, b) => {\n const va = (a as Record<string, unknown>)[field]\n const vb = (b as Record<string, unknown>)[field]\n if (va == null && vb == null) return 0\n if (va == null) return dir\n if (vb == null) return -dir\n if (va < vb) return -dir\n if (va > vb) return dir\n return 0\n })\n }\n\n // Pagination\n if (this._offset > 0 || this._limit !== null) {\n const end = this._limit !== null ? this._offset + this._limit : undefined\n items = items.slice(this._offset, end)\n }\n\n // Resolve relations\n if (this._includes.length > 0) {\n items = await this._resolveIncludes(items)\n }\n\n // Enrich with entry metadata\n if (this._withMeta) {\n items = await this._enrichMeta(items)\n }\n\n return items\n }\n\n async count(): Promise<number> {\n const items = await this.all()\n return items.length\n }\n\n async first(): Promise<T | undefined> {\n const items = await this.all()\n return items[0]\n }\n\n private async _resolveIncludes(items: T[]): Promise<T[]> {\n // Prefetch related collections to avoid N+1\n const cache = new Map<string, Map<string, Record<string, unknown>>>()\n\n for (const field of this._includes) {\n // Collect all IDs for this field\n for (const item of items) {\n const val = (item as Record<string, unknown>)[field]\n const ids = Array.isArray(val) ? val : val ? [val] : []\n for (const id of ids) {\n if (typeof id !== 'string') continue\n // We don't know the target model from here, so we use field name as hint\n // In production, model metadata would provide this mapping\n if (!cache.has(field)) {\n try {\n const related = await this._transport.fetch<Record<string, unknown>>(`content/${field}/${this._locale}.json`)\n const map = new Map<string, Record<string, unknown>>()\n for (const [entryId, entry] of Object.entries(related)) {\n map.set(entryId, { id: entryId, ...entry as object })\n }\n cache.set(field, map)\n } catch {\n cache.set(field, new Map())\n }\n break\n }\n }\n }\n }\n\n return items.map(item => {\n const resolved = { ...item }\n const dst = resolved as Record<string, unknown>\n for (const field of this._includes) {\n const related = cache.get(field)\n if (!related) continue\n const val = (item as Record<string, unknown>)[field]\n if (Array.isArray(val)) {\n dst[field] = val.map(id => typeof id === 'string' ? (related.get(id) ?? id) : id)\n } else if (typeof val === 'string') {\n dst[field] = related.get(val) ?? val\n }\n }\n return resolved\n })\n }\n\n private async _enrichMeta(items: T[]): Promise<T[]> {\n let metaMap: Record<string, EntryMeta> = {}\n try {\n metaMap = await this._transport.fetch<Record<string, EntryMeta>>(`meta/${this._modelId}/${this._locale}.json`)\n } catch {\n // No meta available — return items unchanged\n return items\n }\n return items.map(item => {\n const id = (item as Record<string, unknown>).id as string\n const meta = id ? metaMap[id] : undefined\n if (meta) {\n return { ...item, _meta: meta }\n }\n return item\n })\n }\n}\n\n","import type { SingletonDataSource } from './data-source.js'\n\nexport class CdnSingletonAccessor<T extends Record<string, unknown>> {\n private _source: SingletonDataSource<T>\n private _locale: string = 'en'\n\n constructor(source: SingletonDataSource<T>, defaultLocale?: string) {\n this._source = source\n if (defaultLocale) this._locale = defaultLocale\n }\n\n locale(lang: string): this {\n this._locale = lang\n return this\n }\n\n async get(): Promise<T> {\n return this._source.get(this._locale)\n }\n}\n","import type { DictionaryDataSource } from './data-source.js'\n\nexport class CdnDictionaryAccessor {\n private _source: DictionaryDataSource\n private _locale: string = 'en'\n\n constructor(source: DictionaryDataSource, defaultLocale?: string) {\n this._source = source\n if (defaultLocale) this._locale = defaultLocale\n }\n\n locale(lang: string): this {\n this._locale = lang\n return this\n }\n\n async get(): Promise<Record<string, string>>\n async get(key: string): Promise<string | undefined>\n async get(key: string, params: Record<string, string | number>): Promise<string>\n async get(key?: string, params?: Record<string, string | number>): Promise<Record<string, string> | string | undefined> {\n const dict = await this._source.get(this._locale)\n if (key === undefined) return dict\n const value = dict[key]\n if (value === undefined) return undefined\n if (params) return interpolate(value, params)\n return value\n }\n}\n\nfunction interpolate(template: string, params: Record<string, string | number>): string {\n return template.replace(/\\{(\\w+)\\}/g, (match, key: string) => {\n const val = params[key]\n return val !== undefined ? String(val) : match\n })\n}\n","import type { DocumentDataSource } from './data-source.js'\nimport type { WhereOp, WhereClause } from '../shared/where.js'\nimport { applyWhere } from '../shared/where.js'\n\nexport class CdnDocumentQuery<T extends object> {\n private _source: DocumentDataSource<T>\n private _locale: string = 'en'\n private _filters: WhereClause[] = []\n private _sortField: string | null = null\n private _sortOrder: 'asc' | 'desc' = 'asc'\n\n constructor(source: DocumentDataSource<T>, defaultLocale?: string) {\n this._source = source\n if (defaultLocale) this._locale = defaultLocale\n }\n\n locale(lang: string): this {\n this._locale = lang\n return this\n }\n\n where(field: string, op: WhereOp, value: unknown): this {\n this._filters.push({ field, op, value })\n return this\n }\n\n sort(field: string, order: 'asc' | 'desc' = 'asc'): this {\n this._sortField = field\n this._sortOrder = order\n return this\n }\n\n async all(): Promise<T[]> {\n let items = await this._source.getIndex(this._locale)\n\n for (const clause of this._filters) {\n items = items.filter(item => applyWhere(item, clause))\n }\n\n if (this._sortField) {\n const sf = this._sortField\n const dir = this._sortOrder === 'asc' ? 1 : -1\n items = items.toSorted((a, b) => {\n const va = (a as Record<string, unknown>)[sf] as number | string | null | undefined\n const vb = (b as Record<string, unknown>)[sf] as number | string | null | undefined\n if (va == null && vb == null) return 0\n if (va == null) return dir\n if (vb == null) return -dir\n return va < vb ? -dir : va > vb ? dir : 0\n })\n }\n\n return items\n }\n\n async count(): Promise<number> {\n const items = await this.all()\n return items.length\n }\n\n async first(): Promise<T | undefined> {\n const items = await this.all()\n return items[0]\n }\n\n async bySlug(slug: string): Promise<{ frontmatter: T; body: string; html: string } | null> {\n return this._source.getBySlug(slug, this._locale)\n }\n}\n","import type { HttpTransport } from './http-transport.js'\n\nexport interface MediaAssetMeta {\n width?: number\n height?: number\n format?: string\n size?: number\n blurhash?: string | null\n alt?: string | null\n}\n\nexport interface MediaAsset {\n original: string\n variants: Record<string, string>\n meta: MediaAssetMeta\n}\n\nexport interface MediaManifest {\n version: string\n assets: Record<string, MediaAsset>\n}\n\nexport class MediaAccessor {\n private _transport: HttpTransport\n private _manifest: MediaManifest | null = null\n\n constructor(transport: HttpTransport) {\n this._transport = transport\n }\n\n async manifest(): Promise<MediaManifest> {\n if (!this._manifest) {\n this._manifest = await this._transport.fetch<MediaManifest>('_media_manifest.json')\n }\n return this._manifest\n }\n\n async assets(): Promise<Record<string, MediaAsset>> {\n const m = await this.manifest()\n return m.assets\n }\n\n async asset(path: string): Promise<MediaAsset | null> {\n const all = await this.assets()\n return all[path] ?? null\n }\n\n async list(): Promise<Array<{ path: string } & MediaAsset>> {\n const all = await this.assets()\n return Object.entries(all).map(([path, asset]) =>\n Object.assign({ path }, asset),\n )\n }\n\n resolve(asset: MediaAsset, variant?: string): string {\n if (variant && asset.variants[variant]) {\n return asset.variants[variant]\n }\n return asset.original\n }\n\n url(asset: MediaAsset, variant?: string): string {\n const path = this.resolve(asset, variant)\n return this._transport.buildUrl(path)\n }\n}\n","import { ContentrainError } from './errors.js'\n\nexport interface FormFieldConfig {\n id: string\n type: string\n required?: boolean\n label?: string\n placeholder?: string\n options?: Array<{ value: string; label: string }>\n pattern?: string\n min?: number\n max?: number\n}\n\nexport interface FormConfig {\n modelId: string\n fields: FormFieldConfig[]\n captchaType?: 'turnstile' | null\n successMessage?: string\n honeypotField?: string\n}\n\nexport interface FormSubmitResult {\n success: boolean\n message?: string\n errors?: Array<{ field: string; message: string }>\n}\n\nexport interface FormsClientConfig {\n baseUrl: string\n projectId: string\n apiKey?: string\n}\n\nexport class FormsClient {\n private _baseUrl: string\n private _projectId: string\n private _apiKey: string | undefined\n\n constructor(config: FormsClientConfig) {\n this._baseUrl = config.baseUrl.replace(/\\/+$/, '')\n this._projectId = config.projectId\n this._apiKey = config.apiKey\n }\n\n async config(modelId: string): Promise<FormConfig> {\n const url = `${this._baseUrl}/${this._projectId}/${modelId}/config`\n const headers: Record<string, string> = {}\n if (this._apiKey) headers['Authorization'] = `Bearer ${this._apiKey}`\n\n const res = await globalThis.fetch(url, { headers })\n if (!res.ok) throw new ContentrainError(res.status, await res.text())\n return (await res.json()) as FormConfig\n }\n\n async submit(modelId: string, data: Record<string, unknown>, options?: {\n captchaToken?: string\n }): Promise<FormSubmitResult> {\n const url = `${this._baseUrl}/${this._projectId}/${modelId}/submit`\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n }\n if (this._apiKey) headers['Authorization'] = `Bearer ${this._apiKey}`\n\n const body: Record<string, unknown> = { ...data }\n if (options?.captchaToken) {\n body['cf-turnstile-response'] = options.captchaToken\n }\n\n const res = await globalThis.fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n })\n\n const result = (await res.json()) as FormSubmitResult\n\n if (!res.ok && !result.errors) {\n throw new ContentrainError(res.status, result.message ?? 'Form submission failed')\n }\n\n return result\n }\n}\n","import { ContentrainError } from './errors.js'\n\nexport interface ConversationContext {\n activeModelId?: string | null\n activeLocale?: string\n activeEntryId?: string | null\n panelState?: string\n activeBranch?: string | null\n}\n\nexport interface ConversationSendOptions {\n conversationId?: string\n context?: ConversationContext\n}\n\nexport interface ConversationToolResult {\n id: string\n name: string\n result: unknown\n}\n\nexport interface ConversationUsage {\n inputTokens: number\n outputTokens: number\n}\n\nexport interface ConversationResponse {\n conversationId: string\n message: string\n toolResults?: ConversationToolResult[]\n usage: ConversationUsage\n}\n\nexport interface ConversationMessage {\n id: string\n role: 'user' | 'assistant'\n content: string | unknown[]\n toolCalls?: unknown[]\n model?: string\n usage?: ConversationUsage\n createdAt: string\n}\n\nexport interface ConversationHistory {\n conversationId: string\n messages: ConversationMessage[]\n}\n\nexport interface ConversationClientConfig {\n baseUrl: string\n projectId: string\n apiKey: string\n}\n\nexport class ConversationClient {\n private _baseUrl: string\n private _projectId: string\n private _apiKey: string\n\n constructor(config: ConversationClientConfig) {\n this._baseUrl = config.baseUrl.replace(/\\/+$/, '')\n this._projectId = config.projectId\n this._apiKey = config.apiKey\n }\n\n async send(message: string, options?: ConversationSendOptions): Promise<ConversationResponse> {\n const url = `${this._baseUrl}/${this._projectId}/message`\n\n const body: Record<string, unknown> = { message }\n if (options?.conversationId) body.conversationId = options.conversationId\n if (options?.context) body.context = options.context\n\n const res = await globalThis.fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this._apiKey}`,\n },\n body: JSON.stringify(body),\n })\n\n if (!res.ok) {\n throw new ContentrainError(res.status, await res.text())\n }\n\n return (await res.json()) as ConversationResponse\n }\n\n async history(conversationId: string, options?: { limit?: number }): Promise<ConversationHistory> {\n const params = new URLSearchParams({ conversationId })\n if (options?.limit) params.set('limit', String(options.limit))\n\n const url = `${this._baseUrl}/${this._projectId}/history?${params}`\n\n const res = await globalThis.fetch(url, {\n headers: {\n 'Authorization': `Bearer ${this._apiKey}`,\n },\n })\n\n if (!res.ok) {\n throw new ContentrainError(res.status, await res.text())\n }\n\n return (await res.json()) as ConversationHistory\n }\n}\n","import { HttpTransport } from './http-transport.js'\nimport { CdnCollectionQuery } from './collection-query.js'\nimport { CdnSingletonAccessor } from './singleton-accessor.js'\nimport { CdnDictionaryAccessor } from './dictionary-accessor.js'\nimport { CdnDocumentQuery } from './document-query.js'\nimport { MediaAccessor } from './media-accessor.js'\nimport { FormsClient } from './forms-client.js'\nimport { ConversationClient } from './conversation-client.js'\n\nexport interface ContentrainCDNConfig {\n projectId: string\n apiKey: string\n baseUrl?: string\n defaultLocale?: string\n}\n\nexport type ContentrainCDNClient = ReturnType<typeof createContentrain>\n\nexport function createContentrain(config: ContentrainCDNConfig) {\n const transport = new HttpTransport({\n baseUrl: config.baseUrl ?? 'https://studio.contentrain.io/api/cdn/v1',\n projectId: config.projectId,\n apiKey: config.apiKey,\n })\n const defaultLocale = config.defaultLocale\n\n return {\n collection: <T extends object = Record<string, unknown>>(modelId: string) =>\n new CdnCollectionQuery<T>(transport, modelId, defaultLocale),\n\n singleton: <T extends Record<string, unknown> = Record<string, unknown>>(modelId: string) =>\n new CdnSingletonAccessor<T>(transport.singleton<T>(modelId), defaultLocale),\n\n dictionary: (modelId: string) =>\n new CdnDictionaryAccessor(transport.dictionary(modelId), defaultLocale),\n\n document: <T extends object = Record<string, unknown>>(modelId: string) =>\n new CdnDocumentQuery<T>(transport.document<T>(modelId), defaultLocale),\n\n media: () => new MediaAccessor(transport),\n\n form: () => new FormsClient({\n baseUrl: (config.baseUrl ?? 'https://studio.contentrain.io/api/cdn/v1').replace('/cdn/v1', '/forms/v1'),\n projectId: config.projectId,\n apiKey: config.apiKey,\n }),\n\n conversation: () => new ConversationClient({\n baseUrl: (config.baseUrl ?? 'https://studio.contentrain.io/api/cdn/v1').replace('/cdn/v1', '/conversation/v1'),\n projectId: config.projectId,\n apiKey: config.apiKey,\n }),\n\n manifest: () => transport.fetch<unknown>('_manifest.json'),\n models: () => transport.fetch<unknown[]>('models/_index.json'),\n model: (id: string) => transport.fetch<unknown>(`models/${id}.json`),\n }\n}\n\n// Re-exports\nexport { ContentrainError } from './errors.js'\nexport type { CollectionDataSource, SingletonDataSource, DictionaryDataSource, DocumentDataSource } from './data-source.js'\nexport { HttpTransport } from './http-transport.js'\nexport { CdnCollectionQuery } from './collection-query.js'\nexport { CdnSingletonAccessor } from './singleton-accessor.js'\nexport { CdnDictionaryAccessor } from './dictionary-accessor.js'\nexport { CdnDocumentQuery } from './document-query.js'\nexport { MediaAccessor } from './media-accessor.js'\nexport type { MediaAsset, MediaAssetMeta, MediaManifest } from './media-accessor.js'\nexport { FormsClient } from './forms-client.js'\nexport type { FormConfig, FormFieldConfig, FormSubmitResult, FormsClientConfig } from './forms-client.js'\nexport { ConversationClient } from './conversation-client.js'\nexport type {\n ConversationClientConfig,\n ConversationContext,\n ConversationSendOptions,\n ConversationResponse,\n ConversationToolResult,\n ConversationUsage,\n ConversationMessage,\n ConversationHistory,\n} from './conversation-client.js'\n"],"mappings":"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"}
@@ -5,7 +5,9 @@ function applyWhere(item, clause) {
5
5
  case "eq":
6
6
  if (Array.isArray(val)) return val.includes(clause.value);
7
7
  return val === clause.value;
8
- case "ne": return val !== clause.value;
8
+ case "ne":
9
+ if (Array.isArray(val)) return !val.includes(clause.value);
10
+ return val !== clause.value;
9
11
  case "gt": return val > clause.value;
10
12
  case "gte": return val >= clause.value;
11
13
  case "lt": return val < clause.value;
@@ -275,6 +277,8 @@ var CdnDocumentQuery = class {
275
277
  _source;
276
278
  _locale = "en";
277
279
  _filters = [];
280
+ _sortField = null;
281
+ _sortOrder = "asc";
278
282
  constructor(source, defaultLocale) {
279
283
  this._source = source;
280
284
  if (defaultLocale) this._locale = defaultLocale;
@@ -291,9 +295,26 @@ var CdnDocumentQuery = class {
291
295
  });
292
296
  return this;
293
297
  }
298
+ sort(field, order = "asc") {
299
+ this._sortField = field;
300
+ this._sortOrder = order;
301
+ return this;
302
+ }
294
303
  async all() {
295
304
  let items = await this._source.getIndex(this._locale);
296
305
  for (const clause of this._filters) items = items.filter((item) => applyWhere(item, clause));
306
+ if (this._sortField) {
307
+ const sf = this._sortField;
308
+ const dir = this._sortOrder === "asc" ? 1 : -1;
309
+ items = items.toSorted((a, b) => {
310
+ const va = a[sf];
311
+ const vb = b[sf];
312
+ if (va == null && vb == null) return 0;
313
+ if (va == null) return dir;
314
+ if (vb == null) return -dir;
315
+ return va < vb ? -dir : va > vb ? dir : 0;
316
+ });
317
+ }
297
318
  return items;
298
319
  }
299
320
  async count() {
package/dist/cli.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_generate = require("./generate-DAaCl3Np.cjs");
2
+ const require_generate = require("./generate-DN3mAbvC.cjs");
3
3
  let node_path = require("node:path");
4
4
  let node_fs = require("node:fs");
5
5
  //#region src/cli.ts
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as generate } from "./generate-B5P14n43.mjs";
2
+ import { t as generate } from "./generate-mpKW9Y8v.mjs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { watch } from "node:fs";
5
5
  //#region src/cli.ts
@@ -46,6 +46,7 @@ async function readProjectManifest(projectRoot) {
46
46
  domains: rawConfig?.domains ?? [],
47
47
  repository: rawConfig?.repository,
48
48
  assets_path: rawConfig?.assets_path,
49
+ cdn: rawConfig?.cdn,
49
50
  branchRetention: rawConfig?.branchRetention
50
51
  };
51
52
  const modelsDir = (0, node_path.join)(crDir, "models");
@@ -144,7 +145,7 @@ async function mapDocumentLocale(dir, model, locale, strategy) {
144
145
  }
145
146
  //#endregion
146
147
  //#region src/generator/type-emitter.ts
147
- function emitTypes(models) {
148
+ function emitTypes(models, hasMedia = false) {
148
149
  const lines = [
149
150
  "/* eslint-disable */",
150
151
  "/* oxlint-disable */",
@@ -159,12 +160,19 @@ function emitTypes(models) {
159
160
  if (model.kind === "dictionary") lines.push(`export type ${kebabToPascal(model.id)} = Record<string, string>`);
160
161
  else {
161
162
  lines.push(`export interface ${kebabToPascal(model.id)} {`);
162
- if (model.kind === "collection") lines.push(" id: string");
163
+ const baseNames = /* @__PURE__ */ new Set();
164
+ if (model.kind === "collection") {
165
+ lines.push(" id: string");
166
+ baseNames.add("id");
167
+ }
163
168
  if (model.kind === "document") {
164
169
  lines.push(" slug: string");
165
- lines.push(" content: string");
170
+ lines.push(" body: string");
171
+ baseNames.add("slug");
172
+ baseNames.add("body");
166
173
  }
167
174
  if (model.fields) for (const [name, field] of Object.entries(model.fields)) {
175
+ if (baseNames.has(name)) continue;
168
176
  const tsType = fieldToTS(field);
169
177
  const optional = field.required ? "" : "?";
170
178
  lines.push(` ${name}${optional}: ${tsType}`);
@@ -180,7 +188,7 @@ function emitTypes(models) {
180
188
  sort<K extends keyof T>(field: K, order?: 'asc' | 'desc'): QueryBuilder<T>
181
189
  limit(n: number): QueryBuilder<T>
182
190
  offset(n: number): QueryBuilder<T>
183
- include(...fields: string[]): QueryBuilder<T>
191
+ include<K extends keyof T & string>(...fields: K[]): QueryBuilder<T>
184
192
  count(): number
185
193
  first(): T | undefined
186
194
  all(): T[]
@@ -188,7 +196,7 @@ function emitTypes(models) {
188
196
  lines.push("");
189
197
  lines.push(`export interface SingletonAccessor<T> {
190
198
  locale(lang: string): SingletonAccessor<T>
191
- include(...fields: string[]): SingletonAccessor<T>
199
+ include<K extends keyof T & string>(...fields: K[]): SingletonAccessor<T>
192
200
  get(): T
193
201
  }`);
194
202
  lines.push("");
@@ -203,7 +211,8 @@ function emitTypes(models) {
203
211
  locale(lang: string): DocumentQuery<T>
204
212
  where<K extends keyof T>(field: K, value: T[K]): DocumentQuery<T>
205
213
  where<K extends keyof T>(field: K, op: WhereOp, value: unknown): DocumentQuery<T>
206
- include(...fields: string[]): DocumentQuery<T>
214
+ sort<K extends keyof T>(field: K, order?: 'asc' | 'desc'): DocumentQuery<T>
215
+ include<K extends keyof T & string>(...fields: K[]): DocumentQuery<T>
207
216
  bySlug(slug: string): T | undefined
208
217
  count(): number
209
218
  first(): T | undefined
@@ -226,6 +235,11 @@ function emitTypes(models) {
226
235
  for (const m of documents) lines.push(`export declare function document(model: '${m.id}'): DocumentQuery<${kebabToPascal(m.id)}>`);
227
236
  lines.push("export declare function document(model: string): DocumentQuery<Record<string, unknown>>");
228
237
  lines.push("");
238
+ if (hasMedia) {
239
+ lines.push("/** Resolve a stored `media/...` path to its absolute delivery URL. External URLs and already-absolute values pass through unchanged. */");
240
+ lines.push("export declare function media(value: string): string");
241
+ lines.push("");
242
+ }
229
243
  lines.push("export interface ContentrainClient {");
230
244
  for (const m of collections) lines.push(` query(model: '${m.id}'): QueryBuilder<${kebabToPascal(m.id)}>`);
231
245
  lines.push(" query(model: string): QueryBuilder<Record<string, unknown>>");
@@ -259,12 +273,16 @@ function fieldToTS(field) {
259
273
  case "image":
260
274
  case "video":
261
275
  case "file":
262
- case "relation":
276
+ case "relation": {
263
277
  if (Array.isArray(field.model) && field.model.length > 1) return `{ model: ${field.model.map((m) => `'${m}'`).join(" | ")}; ref: string }`;
264
- return "string";
265
- case "relations":
278
+ const target = Array.isArray(field.model) ? field.model[0] : field.model;
279
+ return target ? `string | ${kebabToPascal(target)}` : "string";
280
+ }
281
+ case "relations": {
266
282
  if (Array.isArray(field.model) && field.model.length > 1) return `Array<{ model: ${field.model.map((m) => `'${m}'`).join(" | ")}; ref: string }>`;
267
- return "string[]";
283
+ const target = Array.isArray(field.model) ? field.model[0] : field.model;
284
+ return target ? `Array<string | ${kebabToPascal(target)}>` : "string[]";
285
+ }
268
286
  case "number":
269
287
  case "integer":
270
288
  case "decimal":
@@ -367,12 +385,12 @@ async function emitSingleModule(ref, models) {
367
385
  case "document": {
368
386
  const rawText = await readText(ref.filePath);
369
387
  if (!rawText) return null;
370
- const { frontmatter, body } = parseFrontmatter(rawText);
388
+ const { frontmatter, body } = parseFrontmatter(rawText, stringLikeFieldKeys(model));
371
389
  const slug = ref.slug ?? model.id;
372
390
  const data = {
373
391
  slug,
374
392
  ...frontmatter,
375
- content: body
393
+ body
376
394
  };
377
395
  return {
378
396
  fileName: `${model.id}--${slug}${localeSuffix}.mjs`,
@@ -382,7 +400,34 @@ async function emitSingleModule(ref, models) {
382
400
  default: return null;
383
401
  }
384
402
  }
385
- function parseFrontmatter(text) {
403
+ const STRING_LIKE_TYPES = new Set([
404
+ "string",
405
+ "text",
406
+ "email",
407
+ "url",
408
+ "slug",
409
+ "color",
410
+ "phone",
411
+ "code",
412
+ "icon",
413
+ "markdown",
414
+ "richtext",
415
+ "date",
416
+ "datetime",
417
+ "image",
418
+ "video",
419
+ "file",
420
+ "select",
421
+ "relation"
422
+ ]);
423
+ function stringLikeFieldKeys(model) {
424
+ const keys = /* @__PURE__ */ new Set();
425
+ if (model.fields) {
426
+ for (const [name, field] of Object.entries(model.fields)) if (STRING_LIKE_TYPES.has(field.type)) keys.add(name);
427
+ }
428
+ return keys;
429
+ }
430
+ function parseFrontmatter(text, stringKeys = /* @__PURE__ */ new Set()) {
386
431
  const normalized = text.replace(/\r\n/g, "\n");
387
432
  const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
388
433
  if (!match) return {
@@ -433,25 +478,28 @@ function parseFrontmatter(text) {
433
478
  current[key] = rawValue.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
434
479
  continue;
435
480
  }
436
- current[key] = parseValue(rawValue);
481
+ current[key] = parseValue(rawValue, current === frontmatter && stringKeys.has(key));
437
482
  }
438
483
  return {
439
484
  frontmatter,
440
485
  body
441
486
  };
442
487
  }
443
- function parseValue(raw) {
488
+ function parseValue(raw, forceString = false) {
489
+ const isQuoted = raw.startsWith("\"") && raw.endsWith("\"") || raw.startsWith("'") && raw.endsWith("'");
490
+ const unquoted = isQuoted ? raw.slice(1, -1) : raw;
491
+ if (forceString) return unquoted;
492
+ if (isQuoted) return unquoted;
444
493
  if (raw === "true") return true;
445
494
  if (raw === "false") return false;
446
495
  if (raw === "null") return null;
447
496
  if (/^-?\d+$/.test(raw)) return parseInt(raw, 10);
448
497
  if (/^-?\d+\.\d+$/.test(raw)) return parseFloat(raw);
449
- if (raw.startsWith("\"") && raw.endsWith("\"") || raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1);
450
498
  return raw;
451
499
  }
452
500
  //#endregion
453
501
  //#region src/generator/runtime-emitter.ts
454
- function emitRuntimeModule(models, dataModules, defaultLocale) {
502
+ function emitRuntimeModule(models, dataModules, defaultLocale, cdnBaseUrl) {
455
503
  const lines = [
456
504
  "/* eslint-disable */",
457
505
  "/* oxlint-disable */",
@@ -462,6 +510,10 @@ function emitRuntimeModule(models, dataModules, defaultLocale) {
462
510
  lines.push(`const _defaultLocale = '${defaultLocale}'`);
463
511
  lines.push("");
464
512
  }
513
+ if (cdnBaseUrl) {
514
+ lines.push(`const _mediaBase = ${JSON.stringify(cdnBaseUrl.replace(/\/+$/, ""))}`);
515
+ lines.push("");
516
+ }
465
517
  for (const dm of dataModules) {
466
518
  const varName = fileNameToVar(dm.fileName);
467
519
  lines.push(`import ${varName} from './data/${dm.fileName}'`);
@@ -489,15 +541,17 @@ function emitRuntimeModule(models, dataModules, defaultLocale) {
489
541
  lines.push("// ─── Relation Resolver ───");
490
542
  lines.push("");
491
543
  lines.push("function _resolveEntry(model, id, locale) {");
492
- lines.push(" const localeKey = locale ?? '_default'");
544
+ lines.push(" const localeKeys = locale ? [locale, '_default'] : ['_default']");
545
+ lines.push(" for (const localeKey of localeKeys) {");
493
546
  if (collections.length > 0) {
494
- lines.push(" const colData = _collectionRegistry[model]?.get(localeKey)");
495
- lines.push(" if (colData) { const e = colData.find(x => x.id === id); if (e) return e; }");
547
+ lines.push(" const colData = _collectionRegistry[model]?.get(localeKey)");
548
+ lines.push(" if (colData) { const e = colData.find(x => x.id === id); if (e) return e; }");
496
549
  }
497
550
  if (documents.length > 0) {
498
- lines.push(" const docData = _documentRegistry[model]?.get(localeKey)");
499
- lines.push(" if (docData) { const e = docData.find(x => x.slug === id); if (e) return e; }");
551
+ lines.push(" const docData = _documentRegistry[model]?.get(localeKey)");
552
+ lines.push(" if (docData) { const e = docData.find(x => x.slug === id); if (e) return e; }");
500
553
  }
554
+ lines.push(" }");
501
555
  lines.push(" return undefined");
502
556
  lines.push("}");
503
557
  lines.push("");
@@ -607,31 +661,36 @@ function emitRuntimeModule(models, dataModules, defaultLocale) {
607
661
  lines.push("}");
608
662
  lines.push("");
609
663
  }
664
+ if (cdnBaseUrl) {
665
+ lines.push("// ─── Media ───");
666
+ lines.push("");
667
+ lines.push("export function media(value) {");
668
+ lines.push(" if (typeof value !== 'string' || !value.startsWith('media/')) return value");
669
+ lines.push(" return _mediaBase + '/' + value");
670
+ lines.push("}");
671
+ lines.push("");
672
+ }
610
673
  return lines.join("\n") + "\n";
611
674
  }
612
- function emitCjsWrapper(models) {
675
+ function emitCjsWrapper(models, hasMedia = false) {
613
676
  const exports = [];
614
677
  if (models.some((m) => m.kind === "collection")) exports.push("query");
615
678
  if (models.some((m) => m.kind === "singleton")) exports.push("singleton");
616
679
  if (models.some((m) => m.kind === "dictionary")) exports.push("dictionary");
617
680
  if (models.some((m) => m.kind === "document")) exports.push("document");
681
+ if (hasMedia) exports.push("media");
618
682
  return `/* eslint-disable */
619
683
  /* oxlint-disable */
620
684
  // Auto-generated CJS proxy — delegates to ESM via dynamic import()
621
- // Sync usage: const client = require('#contentrain'); client.query('model')
622
- // Async usage: const client = await require('#contentrain').init()
685
+ // The generated client is ESM-first. From CommonJS, await init() once before
686
+ // accessing exports:
687
+ // const client = await require('#contentrain').init()
688
+ // client.query('model')
689
+ // Prefer native ESM (import) where possible.
623
690
  'use strict'
624
691
  let _mod = null
625
692
  let _promise = null
626
693
 
627
- function _ensure() {
628
- if (_mod) return _mod
629
- throw new Error(
630
- 'Contentrain client not initialized. Call .init() first, then access exports.\\n'
631
- + 'Example: require("#contentrain").init().then(c => c.query("model"))'
632
- )
633
- }
634
-
635
694
  module.exports.init = function() {
636
695
  if (!_promise) _promise = import('./index.mjs').then(function(m) {
637
696
  _mod = m
@@ -701,7 +760,7 @@ function _applyWhere(item, clause) {
701
760
  const val = item[clause.field];
702
761
  switch (clause.op) {
703
762
  case 'eq': return Array.isArray(val) ? val.includes(clause.value) : val === clause.value;
704
- case 'ne': return val !== clause.value;
763
+ case 'ne': return Array.isArray(val) ? !val.includes(clause.value) : val !== clause.value;
705
764
  case 'gt': return val > clause.value;
706
765
  case 'gte': return val >= clause.value;
707
766
  case 'lt': return val < clause.value;
@@ -738,11 +797,12 @@ class QueryBuilder {
738
797
  first() { return this.all()[0]; }
739
798
  _resolveIncludes(item) {
740
799
  const resolved = { ...item };
800
+ const _loc = this._locale ?? this._defaultLocale;
741
801
  for (const field of this._includes) {
742
802
  const meta = this._relationMeta[field]; if (!meta) continue;
743
803
  const targets = Array.isArray(meta.target) ? meta.target : [meta.target];
744
- if (meta.multi) { const ids = item[field]; if (Array.isArray(ids)) { resolved[field] = ids.map(id => { if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id, this._locale); if (r) return r; } return id; } if (typeof id === 'object' && id !== null && 'model' in id && 'ref' in id) { const r = this._resolver(id.model, id.ref, this._locale); if (r) return r; } return id; }); } }
745
- else { const id = item[field]; if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id, this._locale); if (r) { resolved[field] = r; break; } } } else if (typeof id === 'object' && id !== null && 'model' in id && 'ref' in id) { const r = this._resolver(id.model, id.ref, this._locale); if (r) resolved[field] = r; } }
804
+ if (meta.multi) { const ids = item[field]; if (Array.isArray(ids)) { resolved[field] = ids.map(id => { if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id, _loc); if (r) return r; } return id; } if (typeof id === 'object' && id !== null && 'model' in id && 'ref' in id) { const r = this._resolver(id.model, id.ref, _loc); if (r) return r; } return id; }); } }
805
+ else { const id = item[field]; if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id, _loc); if (r) { resolved[field] = r; break; } } } else if (typeof id === 'object' && id !== null && 'model' in id && 'ref' in id) { const r = this._resolver(id.model, id.ref, _loc); if (r) resolved[field] = r; } }
746
806
  }
747
807
  return resolved;
748
808
  }
@@ -780,15 +840,16 @@ class DictionaryAccessor {
780
840
  if (key === undefined) return dict;
781
841
  const val = dict[key];
782
842
  if (val === undefined) return undefined;
783
- if (params) return val.replace(/{(w+)}/g, (m, k) => { const v = params[k]; return v !== undefined ? String(v) : m; });
843
+ if (params) return val.replace(/\\{(\\w+)\\}/g, (m, k) => { const v = params[k]; return v !== undefined ? String(v) : m; });
784
844
  return val;
785
845
  }
786
846
  }
787
847
 
788
848
  class DocumentQuery {
789
- constructor(data, relationMeta, resolver, defaultLocale) { this._data = data; this._filters = []; this._locale = null; this._includes = []; this._relationMeta = relationMeta || {}; this._resolver = resolver || null; this._defaultLocale = defaultLocale || null; }
849
+ constructor(data, relationMeta, resolver, defaultLocale) { this._data = data; this._filters = []; this._sortField = null; this._sortOrder = 'asc'; this._locale = null; this._includes = []; this._relationMeta = relationMeta || {}; this._resolver = resolver || null; this._defaultLocale = defaultLocale || null; }
790
850
  locale(lang) { this._locale = lang; return this; }
791
851
  where(field, opOrValue, value) { if (value !== undefined) { this._filters.push({ field, op: opOrValue, value }); } else { this._filters.push({ field, op: 'eq', value: opOrValue }); } return this; }
852
+ sort(field, order = 'asc') { this._sortField = field; this._sortOrder = order; return this; }
792
853
  include(...fields) { this._includes.push(...fields); return this; }
793
854
  bySlug(slug) {
794
855
  const items = this._resolveData(); const item = items.find(x => x.slug === slug);
@@ -797,15 +858,16 @@ class DocumentQuery {
797
858
  }
798
859
  count() { return this.all().length; }
799
860
  first() { return this.all()[0]; }
800
- all() { let items = this._resolveData(); for (const clause of this._filters) items = items.filter(item => _applyWhere(item, clause)); if (this._includes.length > 0 && this._resolver) { items = items.map(item => this._resolveIncludes(item)); } return items; }
861
+ all() { let items = this._resolveData(); for (const clause of this._filters) items = items.filter(item => _applyWhere(item, clause)); if (this._sortField) { const sf = this._sortField; const d = this._sortOrder === 'asc' ? 1 : -1; items.sort((a, b) => { const va = a[sf], vb = b[sf]; if (va == null && vb == null) return 0; if (va == null) return d; if (vb == null) return -d; return va < vb ? -d : va > vb ? d : 0; }); } if (this._includes.length > 0 && this._resolver) { items = items.map(item => this._resolveIncludes(item)); } return items; }
801
862
  _resolveData() { let key; if (this._locale) { key = this._locale; } else if (this._defaultLocale && this._data.has(this._defaultLocale)) { key = this._defaultLocale; } else { key = this._data.keys().next().value; } return [...(this._data.get(key) ?? [])]; }
802
863
  _resolveIncludes(item) {
803
864
  const resolved = { ...item };
865
+ const _loc = this._locale ?? this._defaultLocale;
804
866
  for (const field of this._includes) {
805
867
  const meta = this._relationMeta[field]; if (!meta) continue;
806
868
  const targets = Array.isArray(meta.target) ? meta.target : [meta.target];
807
- if (meta.multi) { const ids = item[field]; if (Array.isArray(ids)) { resolved[field] = ids.map(id => { if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id, this._locale); if (r) return r; } return id; } if (typeof id === 'object' && id !== null && 'model' in id && 'ref' in id) { const r = this._resolver(id.model, id.ref, this._locale); if (r) return r; } return id; }); } }
808
- else { const id = item[field]; if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id, this._locale); if (r) { resolved[field] = r; break; } } } else if (typeof id === 'object' && id !== null && 'model' in id && 'ref' in id) { const r = this._resolver(id.model, id.ref, this._locale); if (r) resolved[field] = r; } }
869
+ if (meta.multi) { const ids = item[field]; if (Array.isArray(ids)) { resolved[field] = ids.map(id => { if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id, _loc); if (r) return r; } return id; } if (typeof id === 'object' && id !== null && 'model' in id && 'ref' in id) { const r = this._resolver(id.model, id.ref, _loc); if (r) return r; } return id; }); } }
870
+ else { const id = item[field]; if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id, _loc); if (r) { resolved[field] = r; break; } } } else if (typeof id === 'object' && id !== null && 'model' in id && 'ref' in id) { const r = this._resolver(id.model, id.ref, _loc); if (r) resolved[field] = r; } }
809
871
  }
810
872
  return resolved;
811
873
  }
@@ -848,10 +910,12 @@ async function generate(options) {
848
910
  const clientDir = (0, node_path.join)(projectRoot, ".contentrain", "client");
849
911
  const dataDir = (0, node_path.join)(clientDir, "data");
850
912
  const manifest = await readProjectManifest(projectRoot);
913
+ const cdnBaseUrl = options.cdnBaseUrl ?? manifest.config.cdn?.url;
914
+ const hasMedia = Boolean(cdnBaseUrl);
851
915
  const dataModules = await emitDataModules(manifest.models, manifest.contentFiles);
852
- const typesContent = emitTypes(manifest.models);
853
- const runtimeContent = emitRuntimeModule(manifest.models, dataModules, manifest.config.locales.default);
854
- const cjsContent = emitCjsWrapper(manifest.models);
916
+ const typesContent = emitTypes(manifest.models, hasMedia);
917
+ const runtimeContent = emitRuntimeModule(manifest.models, dataModules, manifest.config.locales.default, cdnBaseUrl);
918
+ const cjsContent = emitCjsWrapper(manifest.models, hasMedia);
855
919
  const newFileNames = new Set(dataModules.map((dm) => dm.fileName));
856
920
  try {
857
921
  const existing = await readDir(dataDir);