@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.
- package/README.md +13 -0
- package/dist/cdn/index.cjs +1 -1
- package/dist/cdn/index.d.cts +1 -1
- package/dist/cdn/index.d.mts +1 -1
- package/dist/cdn/index.mjs +1 -1
- package/dist/{cdn-SOuikUcY.mjs → cdn-CNdH7sKk.mjs} +23 -2
- package/dist/cdn-CNdH7sKk.mjs.map +1 -0
- package/dist/{cdn-5ycdk2ET.cjs → cdn-aeCpH9Dt.cjs} +22 -1
- package/dist/cli.cjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/{generate-DAaCl3Np.cjs → generate-DN3mAbvC.cjs} +108 -44
- package/dist/{generate-B5P14n43.mjs → generate-mpKW9Y8v.mjs} +109 -45
- package/dist/generate-mpKW9Y8v.mjs.map +1 -0
- package/dist/generator/generate.cjs +1 -1
- package/dist/generator/generate.d.cts +8 -0
- package/dist/generator/generate.d.cts.map +1 -1
- package/dist/generator/generate.d.mts +8 -0
- package/dist/generator/generate.d.mts.map +1 -1
- package/dist/generator/generate.mjs +1 -1
- package/dist/{index-BChs94uA.d.cts → index-mBPlkuGU.d.cts} +4 -1
- package/dist/index-mBPlkuGU.d.cts.map +1 -0
- package/dist/{index-CsCpovuB.d.mts → index-tyAxQaPt.d.mts} +4 -1
- package/dist/index-tyAxQaPt.d.mts.map +1 -0
- package/dist/index.cjs +24 -3
- package/dist/index.d.cts +4 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +4 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +24 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/skills/contentrain-query/references/bundler-config.md +10 -8
- package/dist/cdn-SOuikUcY.mjs.map +0 -1
- package/dist/generate-B5P14n43.mjs.map +0 -1
- package/dist/index-BChs94uA.d.cts.map +0 -1
- 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(...)`.
|
package/dist/cdn/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_cdn = require("../cdn-
|
|
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;
|
package/dist/cdn/index.d.cts
CHANGED
|
@@ -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-
|
|
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 };
|
package/dist/cdn/index.d.mts
CHANGED
|
@@ -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-
|
|
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 };
|
package/dist/cdn/index.mjs
CHANGED
|
@@ -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-
|
|
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":
|
|
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-
|
|
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":
|
|
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
package/dist/cli.mjs
CHANGED
|
@@ -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
|
-
|
|
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("
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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("
|
|
495
|
-
lines.push("
|
|
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("
|
|
499
|
-
lines.push("
|
|
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
|
-
//
|
|
622
|
-
//
|
|
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,
|
|
745
|
-
else { const id = item[field]; if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id,
|
|
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(
|
|
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,
|
|
808
|
-
else { const id = item[field]; if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id,
|
|
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);
|