@contentrain/query 5.0.1 → 5.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 CHANGED
@@ -173,9 +173,61 @@ Public root exports:
173
173
  - `SingletonAccessor`
174
174
  - `DictionaryAccessor`
175
175
  - `DocumentQuery`
176
- - `createContentrainClient`
176
+ - `createContentrainClient` — local generated client loader
177
+ - `createContentrain` — CDN client factory
178
+ - `ContentrainError` — HTTP error class for CDN mode
177
179
 
178
- ## 🧩 CommonJS Usage
180
+ ## CDN Transport
181
+
182
+ For apps that fetch content from Contentrain Studio CDN (SSR, serverless, mobile):
183
+
184
+ ```ts
185
+ import { createContentrain } from '@contentrain/query/cdn'
186
+
187
+ const client = createContentrain({
188
+ projectId: '350696e8-...',
189
+ apiKey: 'crn_live_xxx',
190
+ // baseUrl: 'https://studio.contentrain.io/api/cdn/v1' (default)
191
+ })
192
+
193
+ // All CDN queries are async
194
+ const posts = await client.collection('faq').locale('en').all()
195
+ const hero = await client.singleton('hero').locale('en').get()
196
+ const t = await client.dictionary('ui').locale('en').get()
197
+ const doc = await client.document('docs').locale('en').bySlug('intro')
198
+ ```
199
+
200
+ CDN collection queries support extended operators:
201
+
202
+ ```ts
203
+ const filtered = await client.collection('faq')
204
+ .locale('en')
205
+ .where('order', 'gt', 5)
206
+ .where('category', 'in', ['general', 'billing'])
207
+ .sort('order', 'desc')
208
+ .limit(10)
209
+ .all()
210
+ ```
211
+
212
+ CDN also exposes metadata endpoints:
213
+
214
+ ```ts
215
+ const manifest = await client.manifest()
216
+ const models = await client.models()
217
+ const model = await client.model('faq')
218
+ ```
219
+
220
+ ### CDN vs Local
221
+
222
+ | Aspect | Local (`#contentrain`) | CDN (`createContentrain()`) |
223
+ |--------|----------------------|---------------------------|
224
+ | Data source | Bundled `.mjs` files | HTTP fetch from CDN |
225
+ | Return type | Sync (`T[]`) | Async (`Promise<T[]>`) |
226
+ | Auth | None | API key required |
227
+ | Caching | In-memory (embedded) | ETag-based HTTP cache |
228
+ | Use case | SSG, build-time | SSR, client-side, serverless |
229
+
230
+ ## CommonJS Usage
179
231
 
180
232
  Generated clients support CommonJS through `init()`:
181
233
 
@@ -210,11 +262,15 @@ npx contentrain-query generate --root /path/to/project
210
262
 
211
263
  Main package:
212
264
 
213
- - `@contentrain/query`
265
+ - `@contentrain/query` — runtime classes + `createContentrain()` CDN factory
214
266
 
215
267
  Generator entry:
216
268
 
217
- - `@contentrain/query/generate`
269
+ - `@contentrain/query/generate` — programmatic generation API
270
+
271
+ CDN transport:
272
+
273
+ - `@contentrain/query/cdn` — CDN client with `HttpTransport`, async query classes
218
274
 
219
275
  ## 🧠 Design Constraints
220
276
 
@@ -0,0 +1,9 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_cdn = require("../cdn-C8uXJe3A.cjs");
3
+ exports.CdnCollectionQuery = require_cdn.CdnCollectionQuery;
4
+ exports.CdnDictionaryAccessor = require_cdn.CdnDictionaryAccessor;
5
+ exports.CdnDocumentQuery = require_cdn.CdnDocumentQuery;
6
+ exports.CdnSingletonAccessor = require_cdn.CdnSingletonAccessor;
7
+ exports.ContentrainError = require_cdn.ContentrainError;
8
+ exports.HttpTransport = require_cdn.HttpTransport;
9
+ exports.createContentrain = require_cdn.createContentrain;
@@ -0,0 +1,2 @@
1
+ import { a as CdnDocumentQuery, c as CdnCollectionQuery, d as DictionaryDataSource, f as DocumentDataSource, i as ContentrainError, l as HttpTransport, n as ContentrainCDNConfig, o as CdnDictionaryAccessor, p as SingletonDataSource, r as createContentrain, s as CdnSingletonAccessor, t as ContentrainCDNClient, u as CollectionDataSource } from "../index-Xjd0isXU.cjs";
2
+ export { CdnCollectionQuery, CdnDictionaryAccessor, CdnDocumentQuery, CdnSingletonAccessor, CollectionDataSource, ContentrainCDNClient, ContentrainCDNConfig, ContentrainError, DictionaryDataSource, DocumentDataSource, HttpTransport, SingletonDataSource, createContentrain };
@@ -0,0 +1,2 @@
1
+ import { a as CdnDocumentQuery, c as CdnCollectionQuery, d as DictionaryDataSource, f as DocumentDataSource, i as ContentrainError, l as HttpTransport, n as ContentrainCDNConfig, o as CdnDictionaryAccessor, p as SingletonDataSource, r as createContentrain, s as CdnSingletonAccessor, t as ContentrainCDNClient, u as CollectionDataSource } from "../index-D5zB3y75.mjs";
2
+ export { CdnCollectionQuery, CdnDictionaryAccessor, CdnDocumentQuery, CdnSingletonAccessor, CollectionDataSource, ContentrainCDNClient, ContentrainCDNConfig, ContentrainError, DictionaryDataSource, DocumentDataSource, HttpTransport, SingletonDataSource, createContentrain };
@@ -0,0 +1,2 @@
1
+ import { a as CdnCollectionQuery, i as CdnSingletonAccessor, n as CdnDocumentQuery, o as HttpTransport, r as CdnDictionaryAccessor, s as ContentrainError, t as createContentrain } from "../cdn-D66-Npqt.mjs";
2
+ export { CdnCollectionQuery, CdnDictionaryAccessor, CdnDocumentQuery, CdnSingletonAccessor, ContentrainError, HttpTransport, createContentrain };
@@ -0,0 +1,351 @@
1
+ //#region src/cdn/errors.ts
2
+ var ContentrainError = class extends Error {
3
+ constructor(status, message) {
4
+ super(message);
5
+ this.status = status;
6
+ this.name = "ContentrainError";
7
+ }
8
+ };
9
+ //#endregion
10
+ //#region src/cdn/http-transport.ts
11
+ var HttpTransport = class {
12
+ _baseUrl;
13
+ _projectId;
14
+ _apiKey;
15
+ _cache = /* @__PURE__ */ new Map();
16
+ constructor(config) {
17
+ this._baseUrl = config.baseUrl.replace(/\/+$/, "");
18
+ this._projectId = config.projectId;
19
+ this._apiKey = config.apiKey;
20
+ }
21
+ async fetch(path) {
22
+ const url = `${this._baseUrl}/${this._projectId}/${path}`;
23
+ const cached = this._cache.get(path);
24
+ const headers = { "Authorization": `Bearer ${this._apiKey}` };
25
+ if (cached?.etag) headers["If-None-Match"] = cached.etag;
26
+ const res = await globalThis.fetch(url, { headers });
27
+ if (res.status === 304 && cached) return cached.data;
28
+ if (!res.ok) throw new ContentrainError(res.status, await res.text());
29
+ const data = await res.json();
30
+ const etag = res.headers.get("etag") ?? "";
31
+ if (etag) this._cache.set(path, {
32
+ data,
33
+ etag
34
+ });
35
+ return data;
36
+ }
37
+ collection(modelId) {
38
+ return {
39
+ getAll: async (locale) => {
40
+ const map = await this.fetch(`content/${modelId}/${locale}.json`);
41
+ return Object.entries(map).map(([id, entry]) => Object.assign({ id }, entry));
42
+ },
43
+ getOne: async (id, locale) => {
44
+ const entry = (await this.fetch(`content/${modelId}/${locale}.json`))[id];
45
+ return entry ? {
46
+ id,
47
+ ...entry
48
+ } : null;
49
+ }
50
+ };
51
+ }
52
+ singleton(modelId) {
53
+ return { get: (locale) => this.fetch(`content/${modelId}/${locale}.json`) };
54
+ }
55
+ dictionary(modelId) {
56
+ return { get: (locale) => this.fetch(`content/${modelId}/${locale}.json`) };
57
+ }
58
+ document(modelId) {
59
+ return {
60
+ getIndex: (locale) => this.fetch(`documents/${modelId}/_index/${locale}.json`),
61
+ getBySlug: (slug, locale) => this.fetch(`documents/${modelId}/${slug}/${locale}.json`)
62
+ };
63
+ }
64
+ };
65
+ //#endregion
66
+ //#region src/cdn/collection-query.ts
67
+ var CdnCollectionQuery = class {
68
+ _transport;
69
+ _source;
70
+ _modelId;
71
+ _locale = "en";
72
+ _filters = [];
73
+ _sortField = null;
74
+ _sortOrder = "asc";
75
+ _limit = null;
76
+ _offset = 0;
77
+ _includes = [];
78
+ constructor(transport, modelId, defaultLocale) {
79
+ this._transport = transport;
80
+ this._source = transport.collection(modelId);
81
+ this._modelId = modelId;
82
+ if (defaultLocale) this._locale = defaultLocale;
83
+ }
84
+ locale(lang) {
85
+ this._locale = lang;
86
+ return this;
87
+ }
88
+ where(field, op, value) {
89
+ this._filters.push({
90
+ field,
91
+ op,
92
+ value
93
+ });
94
+ return this;
95
+ }
96
+ sort(field, order = "asc") {
97
+ this._sortField = field;
98
+ this._sortOrder = order;
99
+ return this;
100
+ }
101
+ limit(n) {
102
+ this._limit = n;
103
+ return this;
104
+ }
105
+ offset(n) {
106
+ this._offset = n;
107
+ return this;
108
+ }
109
+ include(...fields) {
110
+ this._includes.push(...fields);
111
+ return this;
112
+ }
113
+ async all() {
114
+ let items = await this._source.getAll(this._locale);
115
+ for (const clause of this._filters) items = items.filter((item) => applyWhere$1(item, clause));
116
+ if (this._sortField) {
117
+ const field = this._sortField;
118
+ const dir = this._sortOrder === "asc" ? 1 : -1;
119
+ items = items.toSorted((a, b) => {
120
+ const va = a[field];
121
+ const vb = b[field];
122
+ if (va == null && vb == null) return 0;
123
+ if (va == null) return dir;
124
+ if (vb == null) return -dir;
125
+ if (va < vb) return -dir;
126
+ if (va > vb) return dir;
127
+ return 0;
128
+ });
129
+ }
130
+ if (this._offset > 0 || this._limit !== null) {
131
+ const end = this._limit !== null ? this._offset + this._limit : void 0;
132
+ items = items.slice(this._offset, end);
133
+ }
134
+ if (this._includes.length > 0) items = await this._resolveIncludes(items);
135
+ return items;
136
+ }
137
+ async first() {
138
+ return (await this.all())[0];
139
+ }
140
+ async _resolveIncludes(items) {
141
+ const cache = /* @__PURE__ */ new Map();
142
+ for (const field of this._includes) for (const item of items) {
143
+ const val = item[field];
144
+ const ids = Array.isArray(val) ? val : val ? [val] : [];
145
+ for (const id of ids) {
146
+ if (typeof id !== "string") continue;
147
+ if (!cache.has(field)) {
148
+ try {
149
+ const related = await this._transport.fetch(`content/${field}/${this._locale}.json`);
150
+ const map = /* @__PURE__ */ new Map();
151
+ for (const [entryId, entry] of Object.entries(related)) map.set(entryId, {
152
+ id: entryId,
153
+ ...entry
154
+ });
155
+ cache.set(field, map);
156
+ } catch {
157
+ cache.set(field, /* @__PURE__ */ new Map());
158
+ }
159
+ break;
160
+ }
161
+ }
162
+ }
163
+ return items.map((item) => {
164
+ const resolved = { ...item };
165
+ const dst = resolved;
166
+ for (const field of this._includes) {
167
+ const related = cache.get(field);
168
+ if (!related) continue;
169
+ const val = item[field];
170
+ if (Array.isArray(val)) dst[field] = val.map((id) => typeof id === "string" ? related.get(id) ?? id : id);
171
+ else if (typeof val === "string") dst[field] = related.get(val) ?? val;
172
+ }
173
+ return resolved;
174
+ });
175
+ }
176
+ };
177
+ function applyWhere$1(item, clause) {
178
+ const val = item[clause.field];
179
+ switch (clause.op) {
180
+ case "eq": return val === clause.value;
181
+ case "ne": return val !== clause.value;
182
+ case "gt": return val > clause.value;
183
+ case "gte": return val >= clause.value;
184
+ case "lt": return val < clause.value;
185
+ case "lte": return val <= clause.value;
186
+ case "in": return Array.isArray(clause.value) && clause.value.includes(val);
187
+ case "contains":
188
+ if (typeof val === "string") return val.includes(clause.value);
189
+ if (Array.isArray(val)) return val.includes(clause.value);
190
+ return false;
191
+ default: return true;
192
+ }
193
+ }
194
+ //#endregion
195
+ //#region src/cdn/singleton-accessor.ts
196
+ var CdnSingletonAccessor = class {
197
+ _source;
198
+ _locale = "en";
199
+ constructor(source, defaultLocale) {
200
+ this._source = source;
201
+ if (defaultLocale) this._locale = defaultLocale;
202
+ }
203
+ locale(lang) {
204
+ this._locale = lang;
205
+ return this;
206
+ }
207
+ async get() {
208
+ return this._source.get(this._locale);
209
+ }
210
+ };
211
+ //#endregion
212
+ //#region src/cdn/dictionary-accessor.ts
213
+ var CdnDictionaryAccessor = class {
214
+ _source;
215
+ _locale = "en";
216
+ constructor(source, defaultLocale) {
217
+ this._source = source;
218
+ if (defaultLocale) this._locale = defaultLocale;
219
+ }
220
+ locale(lang) {
221
+ this._locale = lang;
222
+ return this;
223
+ }
224
+ async get(key, params) {
225
+ const dict = await this._source.get(this._locale);
226
+ if (key === void 0) return dict;
227
+ const value = dict[key];
228
+ if (value === void 0) return void 0;
229
+ if (params) return interpolate(value, params);
230
+ return value;
231
+ }
232
+ };
233
+ function interpolate(template, params) {
234
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
235
+ const val = params[key];
236
+ return val !== void 0 ? String(val) : match;
237
+ });
238
+ }
239
+ //#endregion
240
+ //#region src/cdn/document-query.ts
241
+ var CdnDocumentQuery = class {
242
+ _source;
243
+ _locale = "en";
244
+ _filters = [];
245
+ constructor(source, defaultLocale) {
246
+ this._source = source;
247
+ if (defaultLocale) this._locale = defaultLocale;
248
+ }
249
+ locale(lang) {
250
+ this._locale = lang;
251
+ return this;
252
+ }
253
+ where(field, op, value) {
254
+ this._filters.push({
255
+ field,
256
+ op,
257
+ value
258
+ });
259
+ return this;
260
+ }
261
+ async all() {
262
+ let items = await this._source.getIndex(this._locale);
263
+ for (const clause of this._filters) items = items.filter((item) => applyWhere(item, clause));
264
+ return items;
265
+ }
266
+ async first() {
267
+ return (await this.all())[0];
268
+ }
269
+ async bySlug(slug) {
270
+ return this._source.getBySlug(slug, this._locale);
271
+ }
272
+ };
273
+ function applyWhere(item, clause) {
274
+ const val = item[clause.field];
275
+ switch (clause.op) {
276
+ case "eq": return val === clause.value;
277
+ case "ne": return val !== clause.value;
278
+ case "gt": return val > clause.value;
279
+ case "gte": return val >= clause.value;
280
+ case "lt": return val < clause.value;
281
+ case "lte": return val <= clause.value;
282
+ case "in": return Array.isArray(clause.value) && clause.value.includes(val);
283
+ case "contains":
284
+ if (typeof val === "string") return val.includes(clause.value);
285
+ if (Array.isArray(val)) return val.includes(clause.value);
286
+ return false;
287
+ default: return true;
288
+ }
289
+ }
290
+ //#endregion
291
+ //#region src/cdn/index.ts
292
+ function createContentrain(config) {
293
+ const transport = new HttpTransport({
294
+ baseUrl: config.baseUrl ?? "https://studio.contentrain.io/api/cdn/v1",
295
+ projectId: config.projectId,
296
+ apiKey: config.apiKey
297
+ });
298
+ const defaultLocale = config.defaultLocale;
299
+ return {
300
+ collection: (modelId) => new CdnCollectionQuery(transport, modelId, defaultLocale),
301
+ singleton: (modelId) => new CdnSingletonAccessor(transport.singleton(modelId), defaultLocale),
302
+ dictionary: (modelId) => new CdnDictionaryAccessor(transport.dictionary(modelId), defaultLocale),
303
+ document: (modelId) => new CdnDocumentQuery(transport.document(modelId), defaultLocale),
304
+ manifest: () => transport.fetch("_manifest.json"),
305
+ models: () => transport.fetch("models/_index.json"),
306
+ model: (id) => transport.fetch(`models/${id}.json`)
307
+ };
308
+ }
309
+ //#endregion
310
+ Object.defineProperty(exports, "CdnCollectionQuery", {
311
+ enumerable: true,
312
+ get: function() {
313
+ return CdnCollectionQuery;
314
+ }
315
+ });
316
+ Object.defineProperty(exports, "CdnDictionaryAccessor", {
317
+ enumerable: true,
318
+ get: function() {
319
+ return CdnDictionaryAccessor;
320
+ }
321
+ });
322
+ Object.defineProperty(exports, "CdnDocumentQuery", {
323
+ enumerable: true,
324
+ get: function() {
325
+ return CdnDocumentQuery;
326
+ }
327
+ });
328
+ Object.defineProperty(exports, "CdnSingletonAccessor", {
329
+ enumerable: true,
330
+ get: function() {
331
+ return CdnSingletonAccessor;
332
+ }
333
+ });
334
+ Object.defineProperty(exports, "ContentrainError", {
335
+ enumerable: true,
336
+ get: function() {
337
+ return ContentrainError;
338
+ }
339
+ });
340
+ Object.defineProperty(exports, "HttpTransport", {
341
+ enumerable: true,
342
+ get: function() {
343
+ return HttpTransport;
344
+ }
345
+ });
346
+ Object.defineProperty(exports, "createContentrain", {
347
+ enumerable: true,
348
+ get: function() {
349
+ return createContentrain;
350
+ }
351
+ });