@dyrected/core 2.0.0 → 2.4.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/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,16 +17,28 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
33
+ createDyrectedApp: () => createDyrectedApp,
23
34
  defineCollection: () => defineCollection,
24
35
  defineConfig: () => defineConfig,
25
36
  defineGlobal: () => defineGlobal,
26
37
  generateAIPrompt: () => generateAIPrompt,
27
- normalizeConfig: () => normalizeConfig
38
+ generateFreshSetupPrompt: () => generateFreshSetupPrompt,
39
+ normalizeConfig: () => normalizeConfig,
40
+ parseMongoWhere: () => parseMongoWhere,
41
+ parseSqlWhere: () => parseSqlWhere
28
42
  });
29
43
  module.exports = __toCommonJS(index_exports);
30
44
 
@@ -36,41 +50,75 @@ function buildEnvironmentSection(frameworkLabel, isSelfHosted, config) {
36
50
  \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
37
51
  1. ENVIRONMENT
38
52
  \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
39
- - Framework : ${frameworkLabel}
53
+ - Framework : ${frameworkLabel || "Detect it"}
40
54
  - Host Type : ${isSelfHosted ? "Self-Hosted (Local/Private Server)" : "Managed (Dyrected Cloud)"}
41
55
  - API Base : ${config.baseUrl || "http://localhost:3000"}
42
56
  ${credentialLines}`;
43
57
  }
44
- function buildDiagnosticSection(existingSite) {
45
- if (existingSite) {
46
- return `
58
+ function buildDiagnosticSection() {
59
+ return `
47
60
  \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
48
- 2. PHASE 0 \u2014 DIAGNOSTIC (EXISTING SITE)
61
+ 2. PHASE 0 \u2014 DISCOVERY
49
62
  \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
50
- Before writing any code, scan the existing codebase and report:
51
- - All hardcoded text strings that should be CMS-managed
52
- - All repeated data structures that should become collections
53
- - All static pages that marketing will want to edit independently
54
- - Any existing fetch or API calls that overlap with Dyrected
63
+ Before writing any code, you MUST ask the user these questions.
64
+ Write them exactly as shown below \u2014 in plain language with examples.
65
+ Wait for the user to answer ALL of them before proceeding.
55
66
 
56
- Then propose a backup plan: extract all current content into a
57
- migration/ folder as structured .md or .json files BEFORE
58
- modifying any code.
67
+ \u2500\u2500\u2500 QUESTION 1 \u2500\u2500\u2500
68
+ Ask:
69
+ "Do you already have a website built, or are we starting fresh?
59
70
 
60
- Do NOT write any implementation code until you have reported
61
- your findings and the user has confirmed the content plan.`;
62
- }
63
- return `
64
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
65
- 2. PHASE 0 \u2014 DISCOVERY (NEW SITE)
66
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
67
- This is a new project. Before designing the content model, ask:
68
- - "What are your core content types? (e.g. Services, Team, Blog, Projects)"
69
- - "Which pages should marketing manage independently with blocks?"
70
- - "Are there any pages that must remain static and never become CMS-managed?"
71
- - "What is the primary goal of the site \u2014 marketing, e-commerce, portfolio, SaaS?"
71
+ \u2192 Already built: share your project or describe what pages you have
72
+ (e.g. Home, About, Services, Contact)
73
+ \u2192 Starting fresh: just say 'new site' and describe what the site is for"
74
+
75
+ \u2500\u2500\u2500 QUESTION 2 \u2500\u2500\u2500
76
+ Ask:
77
+ "What kind of content will your client need to update regularly?
78
+
79
+ Here are some examples to help you think:
80
+ \u2192 Blog posts or news articles
81
+ \u2192 Team member profiles (name, photo, bio)
82
+ \u2192 Services or product descriptions
83
+ \u2192 Testimonials or reviews
84
+ \u2192 Event listings
85
+ \u2192 FAQs
86
+ \u2192 Homepage text (headline, hero image, call-to-action button)
87
+
88
+ Just list the ones that apply. You can say things like:
89
+ 'They need to update blog posts and team members'"
90
+
91
+ \u2500\u2500\u2500 QUESTION 3 \u2500\u2500\u2500
92
+ Ask:
93
+ "Are there any pages on the site that should NEVER change \u2014 like a
94
+ custom-coded page you want to leave exactly as it is?
72
95
 
73
- Do NOT write any code until the user has answered these questions.`;
96
+ Example answer: 'The homepage has a custom animation, leave that alone.
97
+ Everything else can be managed from the CMS.'
98
+
99
+ If everything should be manageable, just say 'all pages'"
100
+
101
+ \u2500\u2500\u2500 QUESTION 4 \u2500\u2500\u2500
102
+ Ask:
103
+ "What is this website for? Pick the closest description:
104
+
105
+ A) A business or agency marketing site (show services, get leads)
106
+ B) A blog or content site (publish articles regularly)
107
+ C) A SaaS or product site (landing page, pricing, features)
108
+ D) A portfolio (show work, case studies)
109
+ E) An e-commerce or product catalogue
110
+ F) Something else \u2014 describe it in one sentence"
111
+
112
+ \u2500\u2500\u2500 IF EXISTING SITE \u2500\u2500\u2500
113
+ If the user confirms they have an existing site, also:
114
+ - Scan the codebase for all hardcoded text that should be CMS-managed
115
+ - Identify repeated data structures that should become collections
116
+ - Propose saving current content to a migration/ folder as .json files
117
+ BEFORE touching any code
118
+ - Report your findings to the user and get confirmation before proceeding
119
+
120
+ Do NOT write any implementation code until all questions are answered
121
+ and the user has confirmed the content plan.`;
74
122
  }
75
123
  function buildConstraintsSection() {
76
124
  return `
@@ -90,7 +138,10 @@ function buildConstraintsSection() {
90
138
  - RESILIENCE : If Dyrected backend is unreachable, fall back to
91
139
  initialData and show stale content \u2014 never an error page.
92
140
  All relationship fields must handle null gracefully.
93
- Every block renderer must have a default fallback case.`;
141
+ Every block renderer must have a default fallback case.
142
+ - AUTO-SEEDING : Use initialData: [...] in CollectionConfig or GlobalConfig
143
+ to automatically populate the database on first run.
144
+ This is great for demo content or default settings.`;
94
145
  }
95
146
  function buildSchemaRulesSection() {
96
147
  return `
@@ -99,8 +150,10 @@ function buildSchemaRulesSection() {
99
150
  \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
100
151
  - Never drop existing fields from the schema. Mark unused fields as deprecated only.
101
152
  - All new fields must have a defaultValue.
102
- - Never rename a field slug \u2014 add a new field and migrate data separately.
103
- - Run npx @dyrected/cli sync:schema after every config change.`;
153
+ - Use renameTo: 'oldName' to lazily migrate data when renaming fields.
154
+ - Use promoted: true for fields that need high-performance SQL indexing or unique constraints.
155
+ - For Cloud deployments, run npx @dyrected/cli sync:schema after every config change. Self-hosted deployments sync automatically on startup.
156
+ `;
104
157
  }
105
158
  function buildDoNotSection() {
106
159
  return `
@@ -111,7 +164,7 @@ function buildDoNotSection() {
111
164
  - Do NOT add custom auth middleware to the admin route.
112
165
  Dyrected handles admin authentication internally. Do not wrap,
113
166
  protect, or redirect the admin route yourself.
114
- - Do NOT use renderAdminUI in a Nuxt project. Use the DyrectedAdmin
167
+ - Do NOT use renderAdminUI in a Nuxt.js project. Use the DyrectedAdmin
115
168
  component which is auto-imported by @dyrected/nuxt.
116
169
  - Do NOT modify or overwrite existing pages without first preserving their data.
117
170
  - Do NOT drop, rename, or remove fields from an existing schema.
@@ -126,24 +179,29 @@ function buildTechnicalReferenceSection() {
126
179
  Use defineCollection, defineGlobal, and defineConfig from '@dyrected/core'.
127
180
 
128
181
  FIELD TYPES:
129
- - Primitive : text | textarea | richText | number | boolean | date | email | url | json
182
+ - Primitive : text | textarea | richText | number | boolean | date | email | url | json | image
130
183
  - Choice : select | multiSelect (requires options: [{ label, value }])
131
184
  - Structural : array | object (requires nested fields: [...])
132
- - Relation : relationship (requires collection: '<slug>')
185
+ - Relation : relationship (requires relationTo: '<slug>')
133
186
  - Media : relationship to an upload collection (e.g. 'media')
134
187
  - Blocks : blocks (requires blocks: [{ slug, labels, fields }])
135
188
 
136
189
  COLLECTION OPTIONS:
137
190
  - upload: true \u2014 media library with file upload support
138
191
  - auth: true \u2014 adds login/me endpoints; password field is auto-managed
192
+ - audit: true \u2014 enables activity logging
139
193
  - admin.useAsTitle \u2014 field used as display title in admin list view
140
194
  - admin.group \u2014 groups collection under a sidebar heading
141
195
  - admin.hidden \u2014 hides collection from the sidebar (internal/system use)
142
196
 
143
197
  FIELD OPTIONS:
198
+ - label \u2014 user-friendly display name (REQUIRED for all fields)
144
199
  - required \u2014 validation
145
200
  - unique \u2014 database-level uniqueness constraint
201
+ - hasMany \u2014 allow multiple values (for relationship, select, image)
146
202
  - defaultValue \u2014 fallback value (required on all new fields added to existing schemas)
203
+ - promoted \u2014 extracts field to a real SQL column for native indexing (SQL adapters only)
204
+ - renameTo \u2014 name of the old field key to migrate data from (lazy migration)
147
205
  - admin.condition \u2014 Jexl expression string to conditionally show/hide field
148
206
  e.g. "status == \\"published\\""
149
207
  - admin.readOnly \u2014 display only, not editable
@@ -164,14 +222,13 @@ Always include a default case in your switch for unknown block types.
164
222
  COMPLETE SCHEMA EXAMPLE:
165
223
  \`\`\`ts
166
224
  import { defineCollection, defineGlobal, defineConfig } from '@dyrected/core'
167
- import { MongoAdapter } from '@dyrected/db-mongodb'
168
- import { S3StorageAdapter } from '@dyrected/storage-s3'
225
+ import { SqliteAdapter } from '@dyrected/db-sqlite'
169
226
 
170
227
  const media = defineCollection({
171
228
  slug: 'media',
172
229
  upload: true,
173
230
  fields: [
174
- { name: 'alt', type: 'text', label: 'Alt Text' },
231
+ { name: 'alt', label: 'Alt Text', type: 'text' },
175
232
  ],
176
233
  })
177
234
 
@@ -179,44 +236,45 @@ const pages = defineCollection({
179
236
  slug: 'pages',
180
237
  admin: { useAsTitle: 'title', group: 'Content' },
181
238
  fields: [
182
- { name: 'title', type: 'text', required: true },
183
- { name: 'slug', type: 'text', required: true, unique: true },
184
- { name: 'seo', type: 'object', fields: [
185
- { name: 'metaTitle', type: 'text' },
186
- { name: 'metaDescription', type: 'textarea' },
187
- { name: 'ogImage', type: 'relationship', collection: 'media' },
239
+ { name: 'title', label: 'Title', type: 'text', required: true },
240
+ { name: 'slug', label: 'URL Slug', type: 'text', required: true, unique: true },
241
+ { name: 'seo', label: 'SEO Metadata', type: 'object', fields: [
242
+ { name: 'metaTitle', label: 'Meta Title', type: 'text' },
243
+ { name: 'metaDescription', label: 'Meta Description', type: 'textarea' },
244
+ { name: 'ogImage', label: 'OG Image', type: 'relationship', relationTo: 'media' },
188
245
  ]},
189
246
  {
190
247
  name: 'layout',
248
+ label: 'Page Layout',
191
249
  type: 'blocks',
192
250
  blocks: [
193
251
  {
194
252
  slug: 'hero',
195
253
  labels: { singular: 'Hero', plural: 'Heroes' },
196
254
  fields: [
197
- { name: 'heading', type: 'text', required: true },
198
- { name: 'subheading', type: 'textarea' },
199
- { name: 'image', type: 'relationship', collection: 'media' },
200
- { name: 'ctaLabel', type: 'text' },
201
- { name: 'ctaLink', type: 'url' },
255
+ { name: 'heading', label: 'Heading', type: 'text', required: true },
256
+ { name: 'subheading', label: 'Subheading', type: 'textarea' },
257
+ { name: 'image', label: 'Hero Image', type: 'relationship', relationTo: 'media' },
258
+ { name: 'ctaLabel', label: 'Button Label', type: 'text' },
259
+ { name: 'ctaLink', label: 'Button Link', type: 'url' },
202
260
  ],
203
261
  },
204
262
  {
205
263
  slug: 'richContent',
206
264
  labels: { singular: 'Rich Content', plural: 'Rich Content Blocks' },
207
265
  fields: [
208
- { name: 'content', type: 'richText', required: true },
266
+ { name: 'content', label: 'Content', type: 'richText', required: true },
209
267
  ],
210
268
  },
211
269
  {
212
270
  slug: 'callToAction',
213
271
  labels: { singular: 'Call to Action', plural: 'Calls to Action' },
214
272
  fields: [
215
- { name: 'heading', type: 'text', required: true },
216
- { name: 'description', type: 'textarea' },
217
- { name: 'buttonLabel', type: 'text' },
218
- { name: 'buttonLink', type: 'url' },
219
- { name: 'theme', type: 'select', options: [
273
+ { name: 'heading', label: 'Heading', type: 'text', required: true },
274
+ { name: 'description', label: 'Description', type: 'textarea' },
275
+ { name: 'buttonLabel', label: 'Button Text', type: 'text' },
276
+ { name: 'buttonLink', label: 'Button Link', type: 'url' },
277
+ { name: 'theme', label: 'Theme', type: 'select', options: [
220
278
  { label: 'Primary', value: 'primary' },
221
279
  { label: 'Secondary', value: 'secondary' },
222
280
  { label: 'Dark', value: 'dark' },
@@ -232,28 +290,18 @@ const settings = defineGlobal({
232
290
  slug: 'settings',
233
291
  label: 'Site Settings',
234
292
  fields: [
235
- { name: 'siteName', type: 'text' },
236
- { name: 'tagline', type: 'text' },
237
- { name: 'logo', type: 'relationship', collection: 'media' },
238
- { name: 'footerText', type: 'textarea' },
293
+ { name: 'siteName', label: 'Site Name', type: 'text' },
294
+ { name: 'tagline', label: 'Site Tagline', type: 'text' },
295
+ { name: 'logo', label: 'Site Logo', type: 'relationship', relationTo: 'media' },
296
+ { name: 'footerText', label: 'Footer Text', type: 'textarea' },
239
297
  ],
240
298
  })
241
299
 
242
300
  export default defineConfig({
243
301
  collections: [media, pages],
244
- globals: [settings],
245
- db: new MongoAdapter({
246
- url: process.env.DATABASE_URL!,
247
- dbName: 'dyrected_cms',
248
- }),
249
- storage: new S3StorageAdapter({
250
- bucket: process.env.S3_BUCKET!,
251
- region: process.env.S3_REGION!,
252
- credentials: {
253
- accessKeyId: process.env.S3_ACCESS_KEY_ID!,
254
- secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
255
- },
256
- }),
302
+ globals: [settings],
303
+ // Use SqliteAdapter for local, PostgresAdapter or MySqlAdapter for production
304
+ db: new SqliteAdapter({ filename: './dyrected.db' }),
257
305
  })
258
306
  \`\`\``;
259
307
  }
@@ -272,7 +320,8 @@ Return your response in exactly this order. Do not combine steps. Do not skip st
272
320
  6. Migration/fallback strategy \u2014 numbered steps
273
321
  7. Schema sync command
274
322
 
275
- API Reference: https://docs.dyrected.com`;
323
+ API Reference: https://docs.dyrected.com
324
+ If you are unsure about any syntax or property, refer to the documentation above.`;
276
325
  }
277
326
  function buildFrameworkSection(activeTab, isSelfHosted, config) {
278
327
  const envPrefix = activeTab === "next" ? "NEXT_PUBLIC_" : activeTab === "nuxt" ? "NUXT_PUBLIC_" : "";
@@ -294,14 +343,14 @@ export const dyrected = createClient({
294
343
  })
295
344
  \`\`\`
296
345
 
297
- 2. Admin Route (app/admin/[[...slug]]/page.tsx):
346
+ 2. Admin Route (app/cms/page.tsx):
298
347
  \`\`\`tsx
299
348
  import { DyrectedAdmin } from '@dyrected/next/admin'
300
349
 
301
350
  export default function AdminPage() {
302
351
  // DyrectedAdmin handles routing, auth, and CSS automatically.
303
352
  // Do NOT wrap this in custom auth middleware.
304
- return <DyrectedAdmin basename="/admin" />
353
+ return <DyrectedAdmin basename="/cms" />
305
354
  }
306
355
  \`\`\`
307
356
 
@@ -326,9 +375,9 @@ export default async function CmsPage({ params }: { params: { slug: string[] } }
326
375
  \`\`\``,
327
376
  nuxt: `
328
377
  \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
329
- 8. IMPLEMENTATION \u2014 Nuxt
378
+ 8. IMPLEMENTATION \u2014 Nuxt.js
330
379
  \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
331
- 1. Nuxt Config (nuxt.config.ts):
380
+ 1. Nuxt.js Config (nuxt.config.ts):
332
381
  \`\`\`ts
333
382
  export default defineNuxtConfig({
334
383
  modules: ['@dyrected/nuxt'],
@@ -340,7 +389,7 @@ export default defineNuxtConfig({
340
389
  })
341
390
  \`\`\`
342
391
 
343
- 2. Admin Route (pages/admin.vue):
392
+ 2. Admin Route (pages/cms/index.vue):
344
393
  \`\`\`vue
345
394
  <script setup lang="ts">
346
395
  // DyrectedAdmin is auto-imported by @dyrected/nuxt.
@@ -352,7 +401,7 @@ definePageMeta({ layout: false })
352
401
 
353
402
  <template>
354
403
  <ClientOnly>
355
- <DyrectedAdmin basename="/admin" />
404
+ <DyrectedAdmin basename="/cms" />
356
405
  </ClientOnly>
357
406
  </template>
358
407
  \`\`\`
@@ -399,7 +448,7 @@ export const dyrected = createClient({
399
448
  })
400
449
  \`\`\`
401
450
 
402
- 2. Admin Route (pages/admin.tsx):
451
+ 2. Admin Route (pages/cms.tsx):
403
452
  \`\`\`tsx
404
453
  import { AdminUI } from '@dyrected/admin'
405
454
  import '@dyrected/admin/styles'
@@ -431,7 +480,7 @@ export const dyrected = createClient({
431
480
  })
432
481
  \`\`\`
433
482
 
434
- 2. Admin Route (pages/admin.vue):
483
+ 2. Admin Route (pages/cms.vue):
435
484
  \`\`\`vue
436
485
  <template>
437
486
  <div ref="container" style="height: 100vh" />
@@ -459,14 +508,350 @@ onUnmounted(() => cleanup?.())
459
508
  };
460
509
  return sections[activeTab] || sections.next;
461
510
  }
511
+ function generateFreshSetupPrompt(activeTab, config) {
512
+ const frameworkLabel = activeTab === "next" ? "Next.js" : activeTab === "nuxt" ? "Nuxt.js" : activeTab ? activeTab.charAt(0).toUpperCase() + activeTab.slice(1) : "the project's detected framework";
513
+ const isSelfHosted = config.isSelfHosted === true || !config.apiKey && !config.siteId;
514
+ const frameworkSetup = {
515
+ nuxt: `
516
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
517
+ 3. INSTALLATION STEPS \u2014 Run these in order
518
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
519
+ Tell the user to follow these steps exactly. Explain each one in plain
520
+ language before showing the command. Do not skip any step.
521
+
522
+ STEP 1 \u2014 Initialize Dyrected
523
+ Tell the user:
524
+ "Run this command in your terminal inside your Nuxt.js project folder.
525
+ It will set everything up for you automatically."
526
+
527
+ \`\`\`bash
528
+ npx @dyrected/cli init
529
+ \`\`\`
530
+
531
+ When it asks questions, tell the user to:
532
+ - Choose: Nuxt.js 3
533
+ - Choose: SQLite (easiest option, no extra setup needed)
534
+
535
+ The CLI will automatically:
536
+ - Install all required packages
537
+ - Create a dyrected.config.ts file
538
+ - Mount the Admin UI at pages/cms/index.vue
539
+ - Generate a .env.example file
540
+
541
+ STEP 2 \u2014 Register the module
542
+ Tell the user to open nuxt.config.ts and add '@dyrected/nuxt' to modules:
543
+
544
+ \`\`\`ts
545
+ export default defineNuxtConfig({
546
+ modules: ['@dyrected/nuxt'],
547
+ })
548
+ \`\`\`
549
+
550
+ STEP 3 \u2014 Set up environment variables
551
+ Tell the user:
552
+ "Find the file called .env.example in your project.
553
+ Make a copy of it and rename the copy to .env
554
+ Then open .env and fill in the values."
555
+
556
+ STEP 4 \u2014 Start the project
557
+ \`\`\`bash
558
+ pnpm dev
559
+ \`\`\`
560
+
561
+ STEP 5 \u2014 Open the dashboard
562
+ Tell the user to open their browser and go to:
563
+ http://localhost:3000/cms
564
+
565
+ If they see the Dyrected admin dashboard, the installation worked.
566
+ Tell them: "You are ready. Now let's set up your content."`,
567
+ next: `
568
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
569
+ 3. INSTALLATION STEPS \u2014 Run these in order
570
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
571
+ Tell the user to follow these steps exactly. Explain each one in plain
572
+ language before showing the command. Do not skip any step.
573
+
574
+ STEP 1 \u2014 Initialize Dyrected
575
+ Tell the user:
576
+ "Run this command in your terminal inside your Next.js project folder."
577
+
578
+ \`\`\`bash
579
+ npx @dyrected/cli init
580
+ \`\`\`
581
+
582
+ When it asks questions, tell the user to:
583
+ - Choose: Next.js
584
+ - Choose: SQLite (easiest option, no extra setup needed)
585
+
586
+ The CLI will automatically:
587
+ - Install all required packages
588
+ - Create a dyrected.config.ts file
589
+ - Mount the Admin UI at app/cms/page.tsx
590
+ - Generate a .env.example file
591
+
592
+ STEP 2 \u2014 Set up environment variables
593
+ Tell the user:
594
+ "Find the file called .env.example in your project.
595
+ Make a copy of it and rename the copy to .env.local
596
+ Then open .env.local and fill in the values."
597
+
598
+ STEP 3 \u2014 Start the project
599
+ \`\`\`bash
600
+ pnpm dev
601
+ \`\`\`
602
+
603
+ STEP 4 \u2014 Open the dashboard
604
+ Tell the user to open their browser and go to:
605
+ http://localhost:3000/cms
606
+
607
+ If they see the Dyrected admin dashboard, the installation worked.
608
+ Tell them: "You are ready. Now let's set up your content."`,
609
+ react: `
610
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
611
+ 3. INSTALLATION STEPS \u2014 Run these in order
612
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
613
+ STEP 1 \u2014 Install the SDK and admin packages
614
+ \`\`\`bash
615
+ npm install @dyrected/sdk @dyrected/admin
616
+ \`\`\`
617
+
618
+ STEP 2 \u2014 Set up the client (lib/dyrected.ts):
619
+ \`\`\`ts
620
+ import { createClient } from '@dyrected/sdk'
621
+
622
+ export const dyrected = createClient({
623
+ baseUrl: '${config.baseUrl || "https://api.dyrected.cloud"}',${isSelfHosted ? "" : `
624
+ apiKey: '${config.apiKey}',
625
+ siteId: '${config.siteId}',`}
626
+ })
627
+ \`\`\`
628
+
629
+ STEP 3 \u2014 Mount the Admin UI (pages/cms.tsx):
630
+ \`\`\`tsx
631
+ import { AdminUI } from '@dyrected/admin'
632
+ import '@dyrected/admin/styles'
633
+
634
+ export default function AdminPage() {
635
+ return (
636
+ <div style={{ height: '100vh' }}>
637
+ <AdminUI
638
+ baseUrl="${config.baseUrl || "https://api.dyrected.cloud"}"${isSelfHosted ? "" : `
639
+ apiKey="${config.apiKey}"
640
+ siteId="${config.siteId}"`}
641
+ />
642
+ </div>
643
+ )
644
+ }
645
+ \`\`\``,
646
+ vue: `
647
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
648
+ 3. INSTALLATION STEPS \u2014 Run these in order
649
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
650
+ STEP 1 \u2014 Install the SDK and admin packages
651
+ \`\`\`bash
652
+ npm install @dyrected/sdk @dyrected/admin
653
+ \`\`\`
654
+
655
+ STEP 2 \u2014 Set up the client (lib/dyrected.ts):
656
+ \`\`\`ts
657
+ import { createClient } from '@dyrected/sdk'
658
+
659
+ export const dyrected = createClient({
660
+ baseUrl: '${config.baseUrl || "https://api.dyrected.cloud"}',${isSelfHosted ? "" : `
661
+ apiKey: '${config.apiKey}',
662
+ siteId: '${config.siteId}',`}
663
+ })
664
+ \`\`\`
665
+
666
+ STEP 3 \u2014 Mount the Admin UI (pages/cms.vue):
667
+ \`\`\`vue
668
+ <template>
669
+ <div ref="container" style="height: 100vh" />
670
+ </template>
671
+
672
+ <script setup>
673
+ import { ref, onMounted, onUnmounted } from 'vue'
674
+ import { renderAdminUI } from '@dyrected/admin'
675
+ import '@dyrected/admin/styles'
676
+
677
+ const container = ref(null)
678
+ let cleanup
679
+
680
+ onMounted(() => {
681
+ cleanup = renderAdminUI(container.value, {
682
+ baseUrl: '${config.baseUrl || "https://api.dyrected.cloud"}',${isSelfHosted ? "" : `
683
+ apiKey: '${config.apiKey}',
684
+ siteId: '${config.siteId}',`}
685
+ })
686
+ })
687
+
688
+ onUnmounted(() => cleanup?.())
689
+ </script>
690
+ \`\`\``
691
+ };
692
+ return [
693
+ `You are a friendly technical assistant helping someone set up Dyrected CMS for the very first time in a ${frameworkLabel} project. They have NOT installed anything yet.
694
+
695
+ Your job is to:
696
+ 1. Understand who you are talking to before giving any instructions
697
+ 2. Ask simple questions about their project and content needs
698
+ 3. Walk them through setup in a way that matches their technical level
699
+ 4. Confirm each step worked before moving to the next
700
+ 5. Help them design their content so their client can manage it
701
+
702
+ Speak in plain language at all times. Never assume technical knowledge.
703
+ Never show more than one step at a time.
704
+ Never mention the terminal, command line, or any commands until you have
705
+ confirmed the user is comfortable running them.`,
706
+ `
707
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
708
+ 1. ENVIRONMENT
709
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
710
+ - Framework : ${frameworkLabel}
711
+ - Host Type : ${isSelfHosted ? "Self-Hosted" : "Dyrected Cloud"}
712
+ - API Base : ${config.baseUrl || "http://localhost:3000"}
713
+ ${isSelfHosted ? "" : `- Site ID : ${config.siteId}
714
+ - API Key : ${config.apiKey}`}`.trim(),
715
+ `
716
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
717
+ 2. PHASE 0 \u2014 UNDERSTAND THE USER FIRST
718
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
719
+ Ask these questions one at a time. Wait for each answer before asking the next.
720
+ Never ask more than one question at a time.
721
+
722
+ \u2500\u2500\u2500 QUESTION 1 \u2014 TECH LEVEL \u2500\u2500\u2500
723
+ This is the most important question. Ask it first.
724
+
725
+ Ask exactly this:
726
+ "Before we dive in \u2014 how would you describe yourself?
727
+
728
+ A) I write code myself (I'm comfortable with the terminal and editing files)
729
+ B) I use tools like Lovable, Bolt, or v0 to build with AI \u2014 I don't write much code myself
730
+ C) I'm a designer or project manager \u2014 someone else usually handles the technical stuff
731
+ D) Something else \u2014 just describe how you work"
732
+
733
+ Use their answer to decide how to guide them for the rest of the setup:
734
+ - If A \u2192 they are TECHNICAL. You may show terminal commands and code directly.
735
+ - If B \u2192 they are SEMI-TECHNICAL. Explain what each step does before showing
736
+ any code or commands. Ask before running anything in the terminal.
737
+ For Lovable users specifically: guide them to use the built-in
738
+ terminal or ask them to paste code into the right file in their editor.
739
+ - If C \u2192 they are NON-TECHNICAL. Do not show terminal commands at all.
740
+ Generate all the code and config for them. Walk them through
741
+ copy-pasting into specific files by name. If a terminal step is
742
+ unavoidable, warn them first and offer to write the exact command
743
+ with a clear explanation of what it does and where to run it.
744
+ - If D \u2192 ask one follow-up question to understand their workflow before
745
+ deciding which path above fits best.
746
+
747
+ \u2500\u2500\u2500 QUESTION 2 \u2014 PROJECT STATUS \u2500\u2500\u2500
748
+ Ask after Q1 is answered:
749
+
750
+ "Do you already have a ${frameworkLabel} project open,
751
+ or are we starting from scratch?
752
+
753
+ \u2192 Already have a project: tell me what the site is about or
754
+ share the folder name
755
+ \u2192 Starting fresh: just say 'new project' and I will help you
756
+ create one first"
757
+
758
+ \u2500\u2500\u2500 QUESTION 3 \u2014 SITE PURPOSE \u2500\u2500\u2500
759
+ Ask after Q2 is answered:
760
+
761
+ "What kind of website is this?
762
+
763
+ A) A business or agency site (show services, get enquiries)
764
+ B) A blog or news site (publish articles regularly)
765
+ C) A SaaS or product landing page (features, pricing, sign up)
766
+ D) A portfolio (show work and past projects)
767
+ E) Something else \u2014 describe it in one sentence"
768
+
769
+ \u2500\u2500\u2500 QUESTION 4 \u2014 CONTENT NEEDS \u2500\u2500\u2500
770
+ Ask after Q3 is answered:
771
+
772
+ "What will your client need to update themselves \u2014 without calling you?
773
+
774
+ Some examples to help you think:
775
+ \u2192 Blog posts or news articles
776
+ \u2192 Team member profiles (name, photo, bio)
777
+ \u2192 Services or product descriptions
778
+ \u2192 Homepage text (headline, buttons, images)
779
+ \u2192 Testimonials or reviews
780
+ \u2192 FAQs
781
+ \u2192 Event listings or announcements
782
+
783
+ Just list the ones that apply. Or say 'not sure yet'
784
+ and we will figure it out together."
785
+
786
+ \u2500\u2500\u2500 AFTER ALL QUESTIONS \u2500\u2500\u2500
787
+ Once all four questions are answered:
788
+ 1. Summarise what you understood in plain English
789
+ 2. Ask the user to confirm before touching any code
790
+ 3. Then move to the installation steps using the correct path
791
+ for their tech level from Question 1`,
792
+ frameworkSetup[activeTab] || frameworkSetup.nuxt,
793
+ `
794
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
795
+ 4. AFTER INSTALLATION \u2014 CONTENT SETUP
796
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
797
+ Once the dashboard is confirmed working, help the user design their
798
+ content model in dyrected.config.ts based on what they told you in
799
+ Phase 0.
800
+
801
+ RULES for this phase:
802
+ - Use defineCollection and defineConfig from '@dyrected/core'
803
+ - Use client.collection(slug) only \u2014 never client.collections
804
+ - Always use initialData in all data fetches
805
+ - Use a catch-all pages collection for marketing-managed pages
806
+ - Use blocks for flexible page layouts
807
+ - Use type: 'relationship', relationTo: 'collectionSlug' for relations
808
+ - Never throw during render \u2014 fall back to initialData on errors
809
+ - All relationship fields must handle null gracefully
810
+
811
+ CONTENT SETUP DELIVERABLES \u2014 in this order:
812
+ 1. dyrected.config.ts \u2014 complete file based on their answers
813
+ 2. Catch-all page route for CMS-managed pages
814
+ 3. Block components list (names and fields only)
815
+ 4. One example fetch showing how to load content on a page
816
+
817
+ After delivering the config, tell the user to sync their schema.
818
+ For TECHNICAL users show the command directly:
819
+ \`\`\`bash
820
+ npx @dyrected/cli sync:schema
821
+ \`\`\`
822
+ For SEMI-TECHNICAL or NON-TECHNICAL users explain it first:
823
+ "This next step tells Dyrected to read your content setup file
824
+ and prepare the database. Here is the command to run:" then show it.
825
+
826
+ Then ask: "Do you want me to help you connect this content to your
827
+ frontend pages now?"
828
+
829
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
830
+ DO NOT
831
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
832
+ - Do NOT show terminal commands before confirming the user's tech level
833
+ - Do NOT assume the user knows what a terminal, CLI, or package manager is
834
+ - Do NOT show all steps at once \u2014 one step at a time always
835
+ - Do NOT use client.collections \u2014 use client.collection(slug)
836
+ - Do NOT add custom auth middleware to the admin route
837
+ - Do NOT use renderAdminUI in a Nuxt.js project
838
+ - Do NOT skip the confirmation after each installation step
839
+ - Do NOT use jargon without explaining it in plain English first
840
+ - Do NOT assume the installation worked \u2014 ask the user to confirm
841
+
842
+ API Reference: https://docs.dyrected.com
843
+ If you are unsure about any syntax, refer to the official documentation above.`
844
+ ].join("\n");
845
+ }
462
846
  function generateAIPrompt(activeTab, config) {
463
- const frameworkLabel = activeTab === "next" ? "Next.js" : activeTab === "nuxt" ? "Nuxt" : activeTab.charAt(0).toUpperCase() + activeTab.slice(1);
847
+ const frameworkLabel = activeTab === "next" ? "Next.js" : activeTab === "nuxt" ? "Nuxt.js" : activeTab ? activeTab.charAt(0).toUpperCase() + activeTab.slice(1) : "the project's detected framework";
464
848
  const isSelfHosted = config.isSelfHosted === true || !config.apiKey && !config.siteId;
465
849
  const existingSite = config.existingSite ?? false;
850
+ const missionText = existingSite ? `You are a Senior Content Architect. Your mission is to integrate Dyrected CMS into an EXISTING ${frameworkLabel} project. Your absolute priority is DATA PRESERVATION and MIGRATION of existing hardcoded content into a flexible, blocks-based schema that empowers marketing teams to move independently.` : `You are a Senior Content Architect. Your mission is to integrate Dyrected CMS into a NEW ${frameworkLabel} project. Your priority is DATA PRESERVATION and creating a CMS that empowers marketing teams to move independently without raising tickets to engineering.`;
466
851
  const sections = [
467
- `You are a Senior Content Architect. Your mission is to integrate Dyrected CMS into a ${frameworkLabel} project. Your priority is DATA PRESERVATION and creating a CMS that empowers marketing teams to move independently without raising tickets to engineering.`,
852
+ missionText,
468
853
  buildEnvironmentSection(frameworkLabel, isSelfHosted, config),
469
- buildDiagnosticSection(existingSite),
854
+ buildDiagnosticSection(),
470
855
  buildConstraintsSection(),
471
856
  buildSchemaRulesSection(),
472
857
  buildDoNotSection(),
@@ -540,6 +925,1952 @@ function normalizeConfig(config) {
540
925
  };
541
926
  }
542
927
 
928
+ // src/utils/parse-where.ts
929
+ function assertNever(op, context) {
930
+ throw new Error(`[dyrected/core] Unhandled where operator "${op}" in ${context}`);
931
+ }
932
+ function parseSqlWhere(where, getJsonField, placeholder = "?") {
933
+ const params = [];
934
+ let pgIndex = 1;
935
+ function next() {
936
+ return placeholder === "pg" ? `$${pgIndex++}` : "?";
937
+ }
938
+ function col(field) {
939
+ return field === "id" ? "id" : getJsonField(field);
940
+ }
941
+ function buildOperator(field, value) {
942
+ const c = col(field);
943
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
944
+ params.push(value);
945
+ return `${c} = ${next()}`;
946
+ }
947
+ const entries = Object.entries(value);
948
+ if (entries.length !== 1) {
949
+ return entries.map(([op2, operand2]) => buildSingleOp(c, op2, operand2)).join(" AND ");
950
+ }
951
+ const [op, operand] = entries[0];
952
+ return buildSingleOp(c, op, operand);
953
+ }
954
+ function buildSingleOp(c, op, operand) {
955
+ switch (op) {
956
+ case "equals":
957
+ params.push(operand);
958
+ return `${c} = ${next()}`;
959
+ case "not_equals":
960
+ params.push(operand);
961
+ return `${c} != ${next()}`;
962
+ case "in": {
963
+ const vals = Array.isArray(operand) ? operand : [operand];
964
+ if (vals.length === 0) return "1=0";
965
+ const placeholders = vals.map((v) => {
966
+ params.push(v);
967
+ return next();
968
+ });
969
+ return `${c} IN (${placeholders.join(", ")})`;
970
+ }
971
+ case "not_in": {
972
+ const vals = Array.isArray(operand) ? operand : [operand];
973
+ if (vals.length === 0) return "1=1";
974
+ const placeholders = vals.map((v) => {
975
+ params.push(v);
976
+ return next();
977
+ });
978
+ return `${c} NOT IN (${placeholders.join(", ")})`;
979
+ }
980
+ case "gt":
981
+ params.push(operand);
982
+ return `${c} > ${next()}`;
983
+ case "gte":
984
+ params.push(operand);
985
+ return `${c} >= ${next()}`;
986
+ case "lt":
987
+ params.push(operand);
988
+ return `${c} < ${next()}`;
989
+ case "lte":
990
+ params.push(operand);
991
+ return `${c} <= ${next()}`;
992
+ case "contains":
993
+ params.push(`%${operand}%`);
994
+ return `${c} LIKE ${next()}`;
995
+ case "starts_with":
996
+ params.push(`${operand}%`);
997
+ return `${c} LIKE ${next()}`;
998
+ case "exists":
999
+ return operand ? `${c} IS NOT NULL` : `${c} IS NULL`;
1000
+ default:
1001
+ return assertNever(op, "parseSqlWhere");
1002
+ }
1003
+ }
1004
+ function buildClause(w) {
1005
+ const parts = [];
1006
+ for (const [field, value] of Object.entries(w)) {
1007
+ if (field === "OR" && Array.isArray(value)) {
1008
+ const sub = value.map((v) => `(${buildClause(v)})`).join(" OR ");
1009
+ parts.push(`(${sub})`);
1010
+ } else if (field === "AND" && Array.isArray(value)) {
1011
+ const sub = value.map((v) => `(${buildClause(v)})`).join(" AND ");
1012
+ parts.push(`(${sub})`);
1013
+ } else {
1014
+ parts.push(buildOperator(field, value));
1015
+ }
1016
+ }
1017
+ return parts.length ? parts.join(" AND ") : "1=1";
1018
+ }
1019
+ const sql = buildClause(where);
1020
+ return { sql, params };
1021
+ }
1022
+ function parseMongoWhere(where) {
1023
+ function buildOperator(field, value) {
1024
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1025
+ return { [field]: { $eq: value } };
1026
+ }
1027
+ const entries = Object.entries(value);
1028
+ if (entries.length !== 1) {
1029
+ const merged = {};
1030
+ for (const [op2, operand2] of entries) {
1031
+ Object.assign(merged, buildSingleOp(field, op2, operand2)[field]);
1032
+ }
1033
+ return { [field]: merged };
1034
+ }
1035
+ const [op, operand] = entries[0];
1036
+ return buildSingleOp(field, op, operand);
1037
+ }
1038
+ function buildSingleOp(field, op, operand) {
1039
+ switch (op) {
1040
+ case "equals":
1041
+ return { [field]: { $eq: operand } };
1042
+ case "not_equals":
1043
+ return { [field]: { $ne: operand } };
1044
+ case "in":
1045
+ return { [field]: { $in: Array.isArray(operand) ? operand : [operand] } };
1046
+ case "not_in":
1047
+ return { [field]: { $nin: Array.isArray(operand) ? operand : [operand] } };
1048
+ case "gt":
1049
+ return { [field]: { $gt: operand } };
1050
+ case "gte":
1051
+ return { [field]: { $gte: operand } };
1052
+ case "lt":
1053
+ return { [field]: { $lt: operand } };
1054
+ case "lte":
1055
+ return { [field]: { $lte: operand } };
1056
+ case "contains":
1057
+ return { [field]: { $regex: operand, $options: "i" } };
1058
+ case "starts_with":
1059
+ return { [field]: { $regex: `^${operand}`, $options: "i" } };
1060
+ case "exists":
1061
+ return { [field]: { $exists: operand } };
1062
+ default:
1063
+ return assertNever(op, "parseMongoWhere");
1064
+ }
1065
+ }
1066
+ function buildClause(w) {
1067
+ const conditions = [];
1068
+ for (const [field, value] of Object.entries(w)) {
1069
+ if (field === "OR" && Array.isArray(value)) {
1070
+ conditions.push({ $or: value.map(buildClause) });
1071
+ } else if (field === "AND" && Array.isArray(value)) {
1072
+ conditions.push({ $and: value.map(buildClause) });
1073
+ } else {
1074
+ conditions.push(buildOperator(field, value));
1075
+ }
1076
+ }
1077
+ if (conditions.length === 0) return {};
1078
+ if (conditions.length === 1) return conditions[0];
1079
+ return { $and: conditions };
1080
+ }
1081
+ return buildClause(where);
1082
+ }
1083
+
1084
+ // src/app.ts
1085
+ var import_hono = require("hono");
1086
+ var import_cors = require("hono/cors");
1087
+ var import_request_id = require("hono/request-id");
1088
+
1089
+ // src/services/population.service.ts
1090
+ var PopulationService = class {
1091
+ db;
1092
+ collections;
1093
+ constructor(db, collections) {
1094
+ this.db = db;
1095
+ this.collections = collections;
1096
+ }
1097
+ /**
1098
+ * Recursively populate relationship fields in a document or array of documents.
1099
+ */
1100
+ async populate(args) {
1101
+ const { data, fields, currentDepth, maxDepth } = args;
1102
+ if (currentDepth >= maxDepth || !data) {
1103
+ return data;
1104
+ }
1105
+ if (Array.isArray(data)) {
1106
+ return Promise.all(data.map((item) => this.populate({ ...args, data: item })));
1107
+ }
1108
+ const populatedDoc = { ...data };
1109
+ for (const field of fields) {
1110
+ const value = populatedDoc[field.name];
1111
+ if (field.type === "relationship" && field.relationTo && value) {
1112
+ const relatedCollection = this.collections.find((c) => c.slug === field.relationTo);
1113
+ if (!relatedCollection) continue;
1114
+ if (Array.isArray(value)) {
1115
+ populatedDoc[field.name] = await Promise.all(
1116
+ value.map(async (id) => {
1117
+ if (!id) return id;
1118
+ let doc = id;
1119
+ if (typeof id === "string") {
1120
+ doc = await this.db.findOne({ collection: field.relationTo, id });
1121
+ }
1122
+ if (!doc || typeof doc !== "object") return id;
1123
+ return this.populate({
1124
+ data: doc,
1125
+ fields: relatedCollection.fields,
1126
+ currentDepth: currentDepth + 1,
1127
+ maxDepth
1128
+ });
1129
+ })
1130
+ );
1131
+ } else if (value) {
1132
+ let doc = value;
1133
+ if (typeof value === "string") {
1134
+ doc = await this.db.findOne({ collection: field.relationTo, id: value });
1135
+ }
1136
+ if (doc && typeof doc === "object") {
1137
+ populatedDoc[field.name] = await this.populate({
1138
+ data: doc,
1139
+ fields: relatedCollection.fields,
1140
+ currentDepth: currentDepth + 1,
1141
+ maxDepth
1142
+ });
1143
+ }
1144
+ }
1145
+ }
1146
+ if ((field.type === "array" || field.type === "object") && field.fields && value) {
1147
+ populatedDoc[field.name] = await this.populate({
1148
+ data: value,
1149
+ fields: field.fields,
1150
+ currentDepth,
1151
+ // Nested fields don't consume depth, only relationships do
1152
+ maxDepth
1153
+ });
1154
+ }
1155
+ if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
1156
+ populatedDoc[field.name] = await Promise.all(
1157
+ value.map(async (blockData) => {
1158
+ const blockConfig = field.blocks.find((b) => b.slug === blockData.blockType);
1159
+ if (!blockConfig) return blockData;
1160
+ return this.populate({
1161
+ data: blockData,
1162
+ fields: blockConfig.fields,
1163
+ currentDepth,
1164
+ maxDepth
1165
+ });
1166
+ })
1167
+ );
1168
+ }
1169
+ }
1170
+ return populatedDoc;
1171
+ }
1172
+ /**
1173
+ * Helper to populate a PaginatedResult
1174
+ */
1175
+ async populateResult(result, fields, maxDepth) {
1176
+ if (maxDepth <= 0) return result;
1177
+ const populatedDocs = await this.populate({
1178
+ data: result.docs,
1179
+ fields,
1180
+ currentDepth: 0,
1181
+ maxDepth
1182
+ });
1183
+ return {
1184
+ ...result,
1185
+ docs: populatedDocs
1186
+ };
1187
+ }
1188
+ };
1189
+
1190
+ // src/services/defaults.service.ts
1191
+ var DefaultsService = class {
1192
+ /**
1193
+ * Recursively apply default values to a data object based on field definitions.
1194
+ */
1195
+ static apply(fields, data = {}) {
1196
+ const result = { ...data || {} };
1197
+ fields.forEach((field) => {
1198
+ let value = result[field.name];
1199
+ if ((value === void 0 || value === null) && field.renameTo) {
1200
+ const legacyValue = result[field.renameTo];
1201
+ if (legacyValue !== void 0 && legacyValue !== null) {
1202
+ value = legacyValue;
1203
+ result[field.name] = legacyValue;
1204
+ }
1205
+ }
1206
+ if (value === void 0 || value === null) {
1207
+ if (field.defaultValue !== void 0) {
1208
+ result[field.name] = field.defaultValue;
1209
+ } else {
1210
+ if (field.type === "boolean") result[field.name] = false;
1211
+ else if (field.type === "array") result[field.name] = [];
1212
+ else if (field.type === "multiSelect") result[field.name] = [];
1213
+ else if (field.type === "object") {
1214
+ result[field.name] = this.apply(field.fields || [], {});
1215
+ }
1216
+ }
1217
+ } else if (field.type === "object" && field.fields) {
1218
+ result[field.name] = this.apply(field.fields, value);
1219
+ } else if (field.type === "array" && field.fields && Array.isArray(value)) {
1220
+ result[field.name] = value.map((item) => this.apply(field.fields, item));
1221
+ }
1222
+ });
1223
+ return result;
1224
+ }
1225
+ };
1226
+
1227
+ // src/services/audit.service.ts
1228
+ var AuditService = class {
1229
+ /**
1230
+ * Writes a single entry to the __audit collection.
1231
+ * Called without await — runs asynchronously and never blocks the primary operation.
1232
+ */
1233
+ static async log(db, args) {
1234
+ try {
1235
+ await db.create({
1236
+ collection: "__audit",
1237
+ data: {
1238
+ collection: args.collection,
1239
+ documentId: args.documentId ?? null,
1240
+ operation: args.operation,
1241
+ user: args.user?.id ?? null,
1242
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1243
+ changes: JSON.stringify({
1244
+ before: args.before ?? null,
1245
+ after: args.after ?? null
1246
+ })
1247
+ }
1248
+ });
1249
+ } catch (err) {
1250
+ console.error("[dyrected/audit] Failed to write audit log:", err);
1251
+ }
1252
+ }
1253
+ };
1254
+
1255
+ // src/controllers/collection.controller.ts
1256
+ var CollectionController = class {
1257
+ collection;
1258
+ constructor(collection) {
1259
+ this.collection = collection;
1260
+ }
1261
+ async find(c) {
1262
+ const config = c.get("config");
1263
+ const db = config.db;
1264
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1265
+ const limit = Number(c.req.query("limit")) || 10;
1266
+ const page = Number(c.req.query("page")) || 1;
1267
+ const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
1268
+ const sort = c.req.query("sort") || void 0;
1269
+ let where = void 0;
1270
+ const whereRaw = c.req.query("where");
1271
+ if (whereRaw) {
1272
+ try {
1273
+ where = JSON.parse(decodeURIComponent(whereRaw));
1274
+ } catch {
1275
+ }
1276
+ }
1277
+ let result = await db.find({
1278
+ collection: this.collection.slug,
1279
+ limit,
1280
+ page,
1281
+ sort,
1282
+ where
1283
+ });
1284
+ if (result.total === 0 && this.collection.initialData && !where && page === 1) {
1285
+ console.log(`[dyrected/core] Auto-seeding collection "${this.collection.slug}" from config.initialData`);
1286
+ for (const data of this.collection.initialData) {
1287
+ await db.create({ collection: this.collection.slug, data });
1288
+ }
1289
+ result = await db.find({
1290
+ collection: this.collection.slug,
1291
+ limit,
1292
+ page,
1293
+ sort,
1294
+ where
1295
+ });
1296
+ }
1297
+ result.docs = result.docs.map((doc) => DefaultsService.apply(this.collection.fields, doc));
1298
+ if (depth > 0) {
1299
+ const populationService = new PopulationService(db, config.collections);
1300
+ result = await populationService.populateResult(result, this.collection.fields, depth);
1301
+ }
1302
+ return c.json(result);
1303
+ }
1304
+ async findOne(c) {
1305
+ const config = c.get("config");
1306
+ const db = config.db;
1307
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1308
+ const id = c.req.param("id");
1309
+ const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
1310
+ if (!id) return c.json({ message: "Missing ID" }, 400);
1311
+ const doc = await db.findOne({ collection: this.collection.slug, id });
1312
+ if (!doc) return c.json({ message: "Not Found" }, 404);
1313
+ const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
1314
+ if (depth > 0 && docWithDefaults) {
1315
+ const populationService = new PopulationService(db, config.collections);
1316
+ const populatedDoc = await populationService.populate({
1317
+ data: docWithDefaults,
1318
+ fields: this.collection.fields,
1319
+ currentDepth: 0,
1320
+ maxDepth: depth
1321
+ });
1322
+ return c.json(populatedDoc);
1323
+ }
1324
+ return c.json(docWithDefaults);
1325
+ }
1326
+ async create(c) {
1327
+ const config = c.get("config");
1328
+ const db = config.db;
1329
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1330
+ const contentType = c.req.header("Content-Type") || "";
1331
+ if (contentType.toLowerCase().includes("multipart/form-data")) {
1332
+ return this.upload(c);
1333
+ }
1334
+ const body = await c.req.json();
1335
+ const user = c.get("user");
1336
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1337
+ const data = {
1338
+ ...body,
1339
+ createdAt: now,
1340
+ updatedAt: now,
1341
+ createdBy: user?.sub ?? null,
1342
+ updatedBy: user?.sub ?? null
1343
+ };
1344
+ const doc = await db.create({ collection: this.collection.slug, data });
1345
+ if (this.collection.audit && db) {
1346
+ AuditService.log(db, {
1347
+ operation: "create",
1348
+ collection: this.collection.slug,
1349
+ documentId: doc.id,
1350
+ user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
1351
+ before: null,
1352
+ after: doc
1353
+ });
1354
+ }
1355
+ return c.json(doc, 201);
1356
+ }
1357
+ async upload(c) {
1358
+ const config = c.get("config");
1359
+ const storage = config.storage;
1360
+ if (!storage) return c.json({ message: "Storage not configured" }, 500);
1361
+ const formData = await c.req.formData();
1362
+ const file = formData.get("file");
1363
+ if (!file) return c.json({ message: "No file uploaded" }, 400);
1364
+ const buffer = new Uint8Array(await file.arrayBuffer());
1365
+ const siteId = c.get("siteId");
1366
+ const workspaceId = c.get("workspaceId");
1367
+ const prefix = workspaceId ? `${workspaceId}/${siteId}` : siteId;
1368
+ const fileData = await storage.upload({
1369
+ filename: file.name,
1370
+ buffer,
1371
+ mimeType: file.type,
1372
+ prefix
1373
+ });
1374
+ const otherData = {};
1375
+ formData.forEach((value, key) => {
1376
+ if (key !== "file" && typeof value === "string") {
1377
+ otherData[key] = value;
1378
+ }
1379
+ });
1380
+ const user = c.get("user");
1381
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1382
+ const doc = await config.db.create({
1383
+ collection: this.collection.slug,
1384
+ data: {
1385
+ ...otherData,
1386
+ ...fileData,
1387
+ createdAt: now,
1388
+ updatedAt: now,
1389
+ createdBy: user?.sub ?? null,
1390
+ updatedBy: user?.sub ?? null
1391
+ }
1392
+ });
1393
+ return c.json(doc, 201);
1394
+ }
1395
+ async update(c) {
1396
+ const config = c.get("config");
1397
+ const db = config.db;
1398
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1399
+ const id = c.req.param("id");
1400
+ if (!id) return c.json({ message: "Missing ID" }, 400);
1401
+ const body = await c.req.json();
1402
+ const user = c.get("user");
1403
+ const data = {
1404
+ ...body,
1405
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1406
+ updatedBy: user?.sub ?? null
1407
+ };
1408
+ let before = null;
1409
+ if (this.collection.audit) {
1410
+ before = await db.findOne({ collection: this.collection.slug, id });
1411
+ }
1412
+ const doc = await db.update({ collection: this.collection.slug, id, data });
1413
+ if (this.collection.audit && db) {
1414
+ AuditService.log(db, {
1415
+ operation: "update",
1416
+ collection: this.collection.slug,
1417
+ documentId: id,
1418
+ user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
1419
+ before,
1420
+ after: doc
1421
+ });
1422
+ }
1423
+ return c.json(doc);
1424
+ }
1425
+ async delete(c) {
1426
+ const config = c.get("config");
1427
+ const db = config.db;
1428
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1429
+ const id = c.req.param("id");
1430
+ if (!id) return c.json({ message: "Missing ID" }, 400);
1431
+ const user = c.get("user");
1432
+ let before = null;
1433
+ if (this.collection.audit) {
1434
+ before = await db.findOne({ collection: this.collection.slug, id });
1435
+ }
1436
+ await db.delete({ collection: this.collection.slug, id });
1437
+ if (this.collection.audit && db) {
1438
+ AuditService.log(db, {
1439
+ operation: "delete",
1440
+ collection: this.collection.slug,
1441
+ documentId: id,
1442
+ user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
1443
+ before,
1444
+ after: null
1445
+ });
1446
+ }
1447
+ return c.json({ message: "Deleted" });
1448
+ }
1449
+ async deleteMany(c) {
1450
+ const config = c.get("config");
1451
+ const db = config.db;
1452
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1453
+ const user = c.get("user");
1454
+ let ids = [];
1455
+ try {
1456
+ const body = await c.req.json().catch(() => null);
1457
+ if (body?.ids && Array.isArray(body.ids)) {
1458
+ ids = body.ids;
1459
+ }
1460
+ } catch {
1461
+ }
1462
+ if (!ids.length) {
1463
+ const raw = c.req.queries("ids") ?? c.req.queries("ids[]") ?? [];
1464
+ ids = raw.filter(Boolean);
1465
+ }
1466
+ if (!ids.length) return c.json({ message: "No IDs provided" }, 400);
1467
+ const deleted = [];
1468
+ const failed = [];
1469
+ for (const id of ids) {
1470
+ try {
1471
+ let before = null;
1472
+ if (this.collection.audit) {
1473
+ before = await db.findOne({ collection: this.collection.slug, id });
1474
+ }
1475
+ await db.delete({ collection: this.collection.slug, id });
1476
+ deleted.push(id);
1477
+ if (this.collection.audit) {
1478
+ AuditService.log(db, {
1479
+ operation: "delete",
1480
+ collection: this.collection.slug,
1481
+ documentId: id,
1482
+ user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
1483
+ before,
1484
+ after: null
1485
+ });
1486
+ }
1487
+ } catch (err) {
1488
+ failed.push({ id, error: err?.message ?? "Unknown error" });
1489
+ }
1490
+ }
1491
+ return c.json({
1492
+ message: `Deleted ${deleted.length} document(s)`,
1493
+ deleted,
1494
+ ...failed.length ? { failed } : {}
1495
+ });
1496
+ }
1497
+ async seed(c) {
1498
+ const config = c.get("config");
1499
+ const db = config.db;
1500
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1501
+ const body = await c.req.json();
1502
+ const initialData = body.data;
1503
+ if (!initialData || !Array.isArray(initialData)) {
1504
+ return c.json({ message: "Invalid initial data" }, 400);
1505
+ }
1506
+ const result = await db.find({ collection: this.collection.slug, limit: 1 });
1507
+ if (result.total > 0) {
1508
+ return c.json({ message: "Collection is not empty, skipping seed" });
1509
+ }
1510
+ console.log(`[dyrected/core] Auto-seeding collection: ${this.collection.slug}`);
1511
+ const createdDocs = [];
1512
+ for (const data of initialData) {
1513
+ const doc = await db.create({ collection: this.collection.slug, data });
1514
+ createdDocs.push(doc);
1515
+ }
1516
+ return c.json({ message: "Seed successful", count: createdDocs.length }, 201);
1517
+ }
1518
+ };
1519
+
1520
+ // src/controllers/global.controller.ts
1521
+ var GlobalController = class {
1522
+ global;
1523
+ constructor(global) {
1524
+ this.global = global;
1525
+ }
1526
+ async get(c) {
1527
+ const config = c.get("config");
1528
+ const db = config.db;
1529
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1530
+ const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
1531
+ let data = await db.getGlobal({ slug: this.global.slug });
1532
+ const isEmpty = !data || Object.keys(data).length === 0;
1533
+ if (isEmpty && this.global.initialData) {
1534
+ console.log(`[dyrected/core] Auto-seeding global "${this.global.slug}" from config.initialData`);
1535
+ await db.updateGlobal({ slug: this.global.slug, data: this.global.initialData });
1536
+ data = this.global.initialData;
1537
+ }
1538
+ const dataWithDefaults = DefaultsService.apply(this.global.fields, data);
1539
+ if (depth > 0 && dataWithDefaults) {
1540
+ const populationService = new PopulationService(db, config.collections);
1541
+ const populatedData = await populationService.populate({
1542
+ data: dataWithDefaults,
1543
+ fields: this.global.fields,
1544
+ currentDepth: 1,
1545
+ maxDepth: depth
1546
+ });
1547
+ return c.json(populatedData);
1548
+ }
1549
+ return c.json(dataWithDefaults);
1550
+ }
1551
+ async update(c) {
1552
+ const db = c.get("config").db;
1553
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1554
+ const body = await c.req.json();
1555
+ const data = await db.updateGlobal({ slug: this.global.slug, data: body });
1556
+ return c.json(data);
1557
+ }
1558
+ async seed(c) {
1559
+ const config = c.get("config");
1560
+ const db = config.db;
1561
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1562
+ const body = await c.req.json();
1563
+ const initialData = body.data;
1564
+ if (!initialData) {
1565
+ return c.json({ message: "Invalid initial data" }, 400);
1566
+ }
1567
+ const existing = await db.getGlobal({ slug: this.global.slug });
1568
+ if (existing && Object.keys(existing).length > 0) {
1569
+ return c.json({ message: "Global is not empty, skipping seed" });
1570
+ }
1571
+ console.log(`[dyrected/core] Auto-seeding global: ${this.global.slug}`);
1572
+ await db.updateGlobal({ slug: this.global.slug, data: initialData });
1573
+ return c.json({ message: "Seed successful", data: initialData }, 201);
1574
+ }
1575
+ };
1576
+
1577
+ // src/controllers/media.controller.ts
1578
+ var MediaController = class {
1579
+ collection;
1580
+ constructor(collection = "media") {
1581
+ this.collection = collection;
1582
+ }
1583
+ async upload(c) {
1584
+ const config = c.get("config");
1585
+ const storage = config.storage;
1586
+ const imageService = config.image;
1587
+ if (!storage) {
1588
+ return c.json({ message: "Storage not configured" }, 500);
1589
+ }
1590
+ const body = await c.req.parseBody();
1591
+ const file = body["file"];
1592
+ const focalPointStr = body["focalPoint"];
1593
+ const focalPoint = focalPointStr ? JSON.parse(focalPointStr) : void 0;
1594
+ if (!file) {
1595
+ return c.json({ message: "No file uploaded" }, 400);
1596
+ }
1597
+ const buffer = new Uint8Array(await file.arrayBuffer());
1598
+ const siteId = c.get("siteId");
1599
+ const workspaceId = c.get("workspaceId");
1600
+ const prefix = workspaceId ? `${workspaceId}/${siteId}` : siteId || "default";
1601
+ let imageMetadata = {};
1602
+ let imageSizes = {};
1603
+ if (imageService && file.type.startsWith("image/")) {
1604
+ let colConfig = config.collections.find((col) => col.slug === this.collection);
1605
+ if (!colConfig && config.onSchemaFetch && siteId) {
1606
+ const dynamic = await config.onSchemaFetch(siteId);
1607
+ colConfig = dynamic.collections?.find((col) => col.slug === this.collection);
1608
+ }
1609
+ try {
1610
+ const processed = await imageService.process({
1611
+ buffer,
1612
+ mimeType: file.type,
1613
+ config: colConfig?.upload,
1614
+ focalPoint
1615
+ });
1616
+ imageMetadata = processed.metadata;
1617
+ imageSizes = processed.sizes;
1618
+ } catch (err) {
1619
+ console.error("[MediaController] Image processing failed:", err);
1620
+ }
1621
+ }
1622
+ const fileData = await storage.upload({
1623
+ filename: file.name,
1624
+ buffer,
1625
+ mimeType: file.type,
1626
+ prefix
1627
+ });
1628
+ const finalFileData = {
1629
+ ...fileData,
1630
+ ...imageMetadata,
1631
+ focalPoint,
1632
+ sizes: {}
1633
+ };
1634
+ if (imageSizes) {
1635
+ for (const [sizeName, sizeData] of Object.entries(imageSizes)) {
1636
+ const ext = file.name.split(".").pop();
1637
+ const baseName = file.name.substring(0, file.name.lastIndexOf("."));
1638
+ const sizeFilename = `${baseName}-${sizeName}.${ext}`;
1639
+ try {
1640
+ const sizeFileData = await storage.upload({
1641
+ filename: sizeFilename,
1642
+ buffer: sizeData.buffer,
1643
+ mimeType: file.type,
1644
+ prefix
1645
+ });
1646
+ finalFileData.sizes[sizeName] = {
1647
+ ...sizeFileData,
1648
+ width: sizeData.width,
1649
+ height: sizeData.height
1650
+ };
1651
+ } catch (err) {
1652
+ console.error(`[MediaController] Failed to upload size ${sizeName}:`, err);
1653
+ }
1654
+ }
1655
+ }
1656
+ const db = config.db;
1657
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1658
+ const doc = await db.create({
1659
+ collection: this.collection,
1660
+ data: finalFileData
1661
+ });
1662
+ return c.json(doc, 201);
1663
+ }
1664
+ async find(c) {
1665
+ const db = c.get("config").db;
1666
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1667
+ const limit = Number(c.req.query("limit")) || 10;
1668
+ const page = Number(c.req.query("page")) || 1;
1669
+ const result = await db.find({
1670
+ collection: this.collection,
1671
+ limit,
1672
+ page
1673
+ });
1674
+ return c.json(result);
1675
+ }
1676
+ async delete(c) {
1677
+ const config = c.get("config");
1678
+ const storage = config.storage;
1679
+ const db = config.db;
1680
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1681
+ const id = c.req.param("id");
1682
+ if (!id) return c.json({ message: "Missing ID" }, 400);
1683
+ const doc = await db.findOne({ collection: this.collection, id });
1684
+ if (!doc) return c.json({ message: "Not Found" }, 404);
1685
+ if (storage) {
1686
+ await storage.delete({ filename: doc.filename });
1687
+ if (doc.sizes) {
1688
+ for (const size of Object.values(doc.sizes)) {
1689
+ if (size.filename) {
1690
+ await storage.delete({ filename: size.filename });
1691
+ }
1692
+ }
1693
+ }
1694
+ }
1695
+ await db.delete({ collection: this.collection, id });
1696
+ return c.json({ message: "Deleted" });
1697
+ }
1698
+ async serve(c) {
1699
+ const config = c.get("config");
1700
+ const storage = config.storage;
1701
+ if (!storage || !storage.resolve) {
1702
+ return c.json({ message: "Storage not configured for serving" }, 404);
1703
+ }
1704
+ const filename = c.req.param("filename");
1705
+ if (!filename) return c.json({ message: "Missing filename" }, 400);
1706
+ let res = await storage.resolve({ filename });
1707
+ if (!res && !filename.includes("/")) {
1708
+ res = await storage.resolve({ filename: `default/${filename}` });
1709
+ }
1710
+ if (!res) return c.json({ message: "Not Found" }, 404);
1711
+ c.header("Content-Type", res.mimeType);
1712
+ return c.body(res.buffer);
1713
+ }
1714
+ };
1715
+
1716
+ // src/auth/password.ts
1717
+ var import_node_util = require("util");
1718
+ var import_node_crypto = require("crypto");
1719
+ var scryptAsync = (0, import_node_util.promisify)(import_node_crypto.scrypt);
1720
+ var SALT_LEN = 16;
1721
+ var KEY_LEN = 64;
1722
+ async function hashPassword(plain) {
1723
+ const salt = (0, import_node_crypto.randomBytes)(SALT_LEN).toString("hex");
1724
+ const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
1725
+ return `${salt}:${derivedKey.toString("hex")}`;
1726
+ }
1727
+ async function verifyPassword(plain, stored) {
1728
+ const [salt, storedHash] = stored.split(":");
1729
+ if (!salt || !storedHash) return false;
1730
+ const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
1731
+ const storedBuffer = Buffer.from(storedHash, "hex");
1732
+ if (derivedKey.length !== storedBuffer.length) return false;
1733
+ return (0, import_node_crypto.timingSafeEqual)(derivedKey, storedBuffer);
1734
+ }
1735
+
1736
+ // src/auth/token.ts
1737
+ var import_jose = require("jose");
1738
+ var import_node_util2 = require("util");
1739
+ function getSecret() {
1740
+ const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET;
1741
+ if (!secret) {
1742
+ throw new Error(
1743
+ "[dyrected/core] DYRECTED_JWT_SECRET is not set. Add it to your environment variables to enable auth collections."
1744
+ );
1745
+ }
1746
+ return new import_node_util2.TextEncoder().encode(secret);
1747
+ }
1748
+ var DEFAULT_EXPIRY = "7d";
1749
+ async function signCollectionToken(payload, expiresIn = DEFAULT_EXPIRY) {
1750
+ return new import_jose.SignJWT({ ...payload }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(expiresIn).sign(getSecret());
1751
+ }
1752
+ async function verifyCollectionToken(token) {
1753
+ const { payload } = await (0, import_jose.jwtVerify)(token, getSecret());
1754
+ return payload;
1755
+ }
1756
+
1757
+ // src/services/email.service.ts
1758
+ var _devSend = null;
1759
+ var _devSendPromise = null;
1760
+ async function getDevSend() {
1761
+ if (_devSend) return _devSend;
1762
+ if (_devSendPromise) return _devSendPromise;
1763
+ _devSendPromise = (async () => {
1764
+ try {
1765
+ const nodemailer = await import("nodemailer");
1766
+ const account = await nodemailer.default.createTestAccount();
1767
+ const transport = nodemailer.default.createTransport({
1768
+ host: "smtp.ethereal.email",
1769
+ port: 587,
1770
+ auth: { user: account.user, pass: account.pass }
1771
+ });
1772
+ console.log("[dyrected/core] No email config \u2014 using Ethereal for dev email preview.");
1773
+ console.log(`[dyrected/core] Ethereal login: https://ethereal.email user: ${account.user} pass: ${account.pass}`);
1774
+ _devSend = async ({ to, subject, html }) => {
1775
+ const info = await transport.sendMail({ from: '"Dyrected Dev" <dev@dyrected.local>', to, subject, html });
1776
+ console.log(`[dyrected/core] Email preview URL: ${nodemailer.default.getTestMessageUrl(info)}`);
1777
+ };
1778
+ return _devSend;
1779
+ } catch {
1780
+ console.warn("[dyrected/core] nodemailer not available \u2014 emails will not be sent in dev.");
1781
+ return null;
1782
+ }
1783
+ })();
1784
+ return _devSendPromise;
1785
+ }
1786
+ async function sendEmail(config, payload) {
1787
+ if (config.email) {
1788
+ await config.email.send(payload);
1789
+ return;
1790
+ }
1791
+ if (process.env.NODE_ENV !== "production") {
1792
+ const devSend = await getDevSend();
1793
+ await devSend?.(payload);
1794
+ }
1795
+ }
1796
+ function buildWelcomeEmail(config, args) {
1797
+ const custom = config.email?.templates?.welcome?.(args);
1798
+ return {
1799
+ subject: custom?.subject ?? "Welcome \u2014 your account is ready",
1800
+ html: custom?.html ?? `
1801
+ <div style="font-family:sans-serif;max-width:600px;margin:0 auto">
1802
+ <h2>Welcome!</h2>
1803
+ <p>Your account has been created. You can now log in with:</p>
1804
+ <p><strong>${args.email}</strong></p>
1805
+ </div>`
1806
+ };
1807
+ }
1808
+ function buildInviteEmail(config, args) {
1809
+ const custom = config.email?.templates?.invite?.(args);
1810
+ return {
1811
+ subject: custom?.subject ?? "You've been invited",
1812
+ html: custom?.html ?? `
1813
+ <div style="font-family:sans-serif;max-width:600px;margin:0 auto">
1814
+ <h2>You've been invited</h2>
1815
+ ${args.invitedByEmail ? `<p>Invited by <strong>${args.invitedByEmail}</strong>.</p>` : ""}
1816
+ <p>Use the token below to accept your invitation. It expires in 7 days.</p>
1817
+ <pre style="background:#f4f4f4;padding:12px;border-radius:4px;word-break:break-all">${args.token}</pre>
1818
+ </div>`
1819
+ };
1820
+ }
1821
+ function buildResetPasswordEmail(config, args) {
1822
+ const custom = config.email?.templates?.resetPassword?.(args);
1823
+ return {
1824
+ subject: custom?.subject ?? "Reset your password",
1825
+ html: custom?.html ?? `
1826
+ <div style="font-family:sans-serif;max-width:600px;margin:0 auto">
1827
+ <h2>Reset your password</h2>
1828
+ <p>Use the token below to reset your password. It expires in 1 hour.</p>
1829
+ <pre style="background:#f4f4f4;padding:12px;border-radius:4px;word-break:break-all">${args.token}</pre>
1830
+ </div>`
1831
+ };
1832
+ }
1833
+ function buildPasswordChangedEmail(config, args) {
1834
+ const custom = config.email?.templates?.passwordChanged?.(args);
1835
+ return {
1836
+ subject: custom?.subject ?? "Your password has been changed",
1837
+ html: custom?.html ?? `
1838
+ <div style="font-family:sans-serif;max-width:600px;margin:0 auto">
1839
+ <h2>Password changed</h2>
1840
+ <p>The password for <strong>${args.email}</strong> was just changed.</p>
1841
+ <p>If you did not make this change, please contact support immediately.</p>
1842
+ </div>`
1843
+ };
1844
+ }
1845
+
1846
+ // src/controllers/auth.controller.ts
1847
+ var AuthController = class {
1848
+ collection;
1849
+ constructor(collection) {
1850
+ this.collection = collection;
1851
+ }
1852
+ // ---------------------------------------------------------------------------
1853
+ // GET /init
1854
+ // Checks if the first user needs to be created.
1855
+ // ---------------------------------------------------------------------------
1856
+ async init(c) {
1857
+ const db = c.get("config").db;
1858
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1859
+ const result = await db.find({
1860
+ collection: this.collection.slug,
1861
+ limit: 1
1862
+ });
1863
+ return c.json({
1864
+ initialized: result.total > 0
1865
+ });
1866
+ }
1867
+ // ---------------------------------------------------------------------------
1868
+ // POST /first-user
1869
+ // Creates the first user if none exist.
1870
+ // ---------------------------------------------------------------------------
1871
+ async registerFirstUser(c) {
1872
+ const config = c.get("config");
1873
+ const db = config.db;
1874
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1875
+ const check = await db.find({
1876
+ collection: this.collection.slug,
1877
+ limit: 1
1878
+ });
1879
+ if (check.total > 0) {
1880
+ return c.json({ error: true, message: "Initial user already exists." }, 403);
1881
+ }
1882
+ const body = await c.req.json().catch(() => null);
1883
+ if (!body?.email || !body?.password) {
1884
+ return c.json({ error: true, message: "email and password are required." }, 400);
1885
+ }
1886
+ const hashedPassword = await hashPassword(body.password);
1887
+ const user = await db.create({
1888
+ collection: this.collection.slug,
1889
+ data: {
1890
+ ...body,
1891
+ password: hashedPassword,
1892
+ roles: ["admin"]
1893
+ // Default first user to admin
1894
+ }
1895
+ });
1896
+ const token = await signCollectionToken({
1897
+ sub: user.id,
1898
+ email: user.email,
1899
+ collection: this.collection.slug
1900
+ });
1901
+ const { subject, html } = buildWelcomeEmail(config, { email: body.email });
1902
+ sendEmail(config, { to: body.email, subject, html }).catch(
1903
+ (err) => console.error("[dyrected/core] Failed to send welcome email:", err)
1904
+ );
1905
+ const { password: _, ...safeUser } = user;
1906
+ return c.json({ token, user: safeUser });
1907
+ }
1908
+ // ---------------------------------------------------------------------------
1909
+ // POST /login
1910
+ // ---------------------------------------------------------------------------
1911
+ async login(c) {
1912
+ const db = c.get("config").db;
1913
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1914
+ const body = await c.req.json().catch(() => null);
1915
+ if (!body?.email || !body?.password) {
1916
+ return c.json({ error: true, message: "email and password are required." }, 400);
1917
+ }
1918
+ const result = await db.find({
1919
+ collection: this.collection.slug,
1920
+ where: { email: body.email },
1921
+ limit: 1
1922
+ });
1923
+ const user = result.docs[0];
1924
+ if (!user) {
1925
+ return c.json({ error: true, message: "Invalid email or password." }, 401);
1926
+ }
1927
+ const valid = await verifyPassword(body.password, user.password);
1928
+ if (!valid) {
1929
+ return c.json({ error: true, message: "Invalid email or password." }, 401);
1930
+ }
1931
+ const token = await signCollectionToken({
1932
+ sub: user.id,
1933
+ email: user.email,
1934
+ collection: this.collection.slug
1935
+ });
1936
+ const { password: _, ...safeUser } = user;
1937
+ return c.json({ token, user: safeUser });
1938
+ }
1939
+ // ---------------------------------------------------------------------------
1940
+ // POST /logout
1941
+ // Auth collections use stateless JWTs — logout is handled client-side.
1942
+ // This endpoint exists so clients have a consistent API surface.
1943
+ // ---------------------------------------------------------------------------
1944
+ async logout(c) {
1945
+ return c.json({ success: true, message: "Logged out. Discard your token." });
1946
+ }
1947
+ // ---------------------------------------------------------------------------
1948
+ // GET /me
1949
+ // ---------------------------------------------------------------------------
1950
+ async me(c) {
1951
+ const db = c.get("config").db;
1952
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1953
+ const requestUser = c.get("user");
1954
+ if (!requestUser) {
1955
+ return c.json({ error: true, message: "Authentication required." }, 401);
1956
+ }
1957
+ const user = await db.findOne({ collection: this.collection.slug, id: requestUser.sub });
1958
+ if (!user) {
1959
+ return c.json({ error: true, message: "User not found." }, 404);
1960
+ }
1961
+ const { password: _, ...safeUser } = user;
1962
+ return c.json(safeUser);
1963
+ }
1964
+ // ---------------------------------------------------------------------------
1965
+ // POST /refresh-token
1966
+ // ---------------------------------------------------------------------------
1967
+ async refreshToken(c) {
1968
+ const requestUser = c.get("user");
1969
+ if (!requestUser) {
1970
+ return c.json({ error: true, message: "Authentication required." }, 401);
1971
+ }
1972
+ const token = await signCollectionToken({
1973
+ sub: requestUser.sub,
1974
+ email: requestUser.email,
1975
+ collection: this.collection.slug
1976
+ });
1977
+ return c.json({ token });
1978
+ }
1979
+ // ---------------------------------------------------------------------------
1980
+ // POST /forgot-password
1981
+ // Requires config.email to be set. Silently succeeds if email not found
1982
+ // to prevent email enumeration.
1983
+ // ---------------------------------------------------------------------------
1984
+ async forgotPassword(c) {
1985
+ const config = c.get("config");
1986
+ const db = config.db;
1987
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1988
+ const body = await c.req.json().catch(() => null);
1989
+ if (!body?.email) {
1990
+ return c.json({ error: true, message: "email is required." }, 400);
1991
+ }
1992
+ const result = await db.find({
1993
+ collection: this.collection.slug,
1994
+ where: { email: body.email },
1995
+ limit: 1
1996
+ });
1997
+ const user = result.docs[0];
1998
+ if (user) {
1999
+ const resetToken = await signCollectionToken(
2000
+ { sub: user.id, email: user.email, collection: this.collection.slug, purpose: "reset" },
2001
+ "1h"
2002
+ );
2003
+ try {
2004
+ const { subject, html } = buildResetPasswordEmail(config, { token: resetToken });
2005
+ await sendEmail(config, { to: user.email, subject, html });
2006
+ } catch (err) {
2007
+ console.error("[dyrected/core] Failed to send password reset email:", err);
2008
+ }
2009
+ }
2010
+ return c.json({
2011
+ success: true,
2012
+ message: "If an account with that email exists, a reset link has been sent."
2013
+ });
2014
+ }
2015
+ // ---------------------------------------------------------------------------
2016
+ // POST /reset-password
2017
+ // Expects { token: string, password: string } in body.
2018
+ // The token is the reset JWT issued by /forgot-password.
2019
+ // ---------------------------------------------------------------------------
2020
+ async resetPassword(c) {
2021
+ const config = c.get("config");
2022
+ const db = config.db;
2023
+ if (!db) return c.json({ message: "Database not configured" }, 500);
2024
+ const body = await c.req.json().catch(() => null);
2025
+ if (!body?.token || !body?.password) {
2026
+ return c.json({ error: true, message: "token and password are required." }, 400);
2027
+ }
2028
+ let payload;
2029
+ try {
2030
+ payload = await verifyCollectionToken(body.token);
2031
+ } catch {
2032
+ return c.json({ error: true, message: "Reset token is invalid or has expired." }, 400);
2033
+ }
2034
+ if (payload.collection !== this.collection.slug || payload.purpose !== "reset") {
2035
+ return c.json({ error: true, message: "Reset token is invalid or has expired." }, 400);
2036
+ }
2037
+ const hashedPassword = await hashPassword(body.password);
2038
+ await db.update({
2039
+ collection: this.collection.slug,
2040
+ id: payload.sub,
2041
+ data: { password: hashedPassword }
2042
+ });
2043
+ const { subject, html } = buildPasswordChangedEmail(config, { email: payload.email });
2044
+ sendEmail(config, { to: payload.email, subject, html }).catch(
2045
+ (err) => console.error("[dyrected/core] Failed to send password-changed email:", err)
2046
+ );
2047
+ return c.json({ success: true, message: "Password has been reset. You can now log in." });
2048
+ }
2049
+ // ---------------------------------------------------------------------------
2050
+ // POST /invite
2051
+ // Requires auth. Issues a signed invite token and emails it to the invitee.
2052
+ // ---------------------------------------------------------------------------
2053
+ async invite(c) {
2054
+ const config = c.get("config");
2055
+ const db = config.db;
2056
+ if (!db) return c.json({ message: "Database not configured" }, 500);
2057
+ const requestUser = c.get("user");
2058
+ if (!requestUser) {
2059
+ return c.json({ error: true, message: "Authentication required." }, 401);
2060
+ }
2061
+ const body = await c.req.json().catch(() => null);
2062
+ if (!body?.email) {
2063
+ return c.json({ error: true, message: "email is required." }, 400);
2064
+ }
2065
+ const existing = await db.find({
2066
+ collection: this.collection.slug,
2067
+ where: { email: body.email },
2068
+ limit: 1
2069
+ });
2070
+ if (existing.total > 0) {
2071
+ return c.json({ error: true, message: "An account with that email already exists." }, 409);
2072
+ }
2073
+ const inviteToken = await signCollectionToken(
2074
+ { sub: body.email, email: body.email, collection: this.collection.slug, purpose: "invite" },
2075
+ "7d"
2076
+ );
2077
+ try {
2078
+ const { subject, html } = buildInviteEmail(config, {
2079
+ token: inviteToken,
2080
+ invitedByEmail: requestUser.email
2081
+ });
2082
+ await sendEmail(config, { to: body.email, subject, html });
2083
+ } catch (err) {
2084
+ console.error("[dyrected/core] Failed to send invite email:", err);
2085
+ }
2086
+ return c.json({ success: true, message: `Invite sent to ${body.email}.` });
2087
+ }
2088
+ // ---------------------------------------------------------------------------
2089
+ // POST /accept-invite
2090
+ // Public. Validates the invite token and creates the user account.
2091
+ // Body: { token, password, ...extraFields }
2092
+ // ---------------------------------------------------------------------------
2093
+ async acceptInvite(c) {
2094
+ const config = c.get("config");
2095
+ const db = config.db;
2096
+ if (!db) return c.json({ message: "Database not configured" }, 500);
2097
+ const body = await c.req.json().catch(() => null);
2098
+ if (!body?.token || !body?.password) {
2099
+ return c.json({ error: true, message: "token and password are required." }, 400);
2100
+ }
2101
+ let payload;
2102
+ try {
2103
+ payload = await verifyCollectionToken(body.token);
2104
+ } catch {
2105
+ return c.json({ error: true, message: "Invite token is invalid or has expired." }, 400);
2106
+ }
2107
+ if (payload.collection !== this.collection.slug || payload.purpose !== "invite") {
2108
+ return c.json({ error: true, message: "Invite token is invalid or has expired." }, 400);
2109
+ }
2110
+ const inviteeEmail = payload.sub;
2111
+ const existing = await db.find({
2112
+ collection: this.collection.slug,
2113
+ where: { email: inviteeEmail },
2114
+ limit: 1
2115
+ });
2116
+ if (existing.total > 0) {
2117
+ return c.json({ error: true, message: "An account with that email already exists." }, 409);
2118
+ }
2119
+ const { token: _t, password: _p, ...extraFields } = body;
2120
+ const hashedPassword = await hashPassword(body.password);
2121
+ const user = await db.create({
2122
+ collection: this.collection.slug,
2123
+ data: { ...extraFields, email: inviteeEmail, password: hashedPassword }
2124
+ });
2125
+ const sessionToken = await signCollectionToken({
2126
+ sub: user.id,
2127
+ email: inviteeEmail,
2128
+ collection: this.collection.slug
2129
+ });
2130
+ const { subject, html } = buildWelcomeEmail(config, { email: inviteeEmail });
2131
+ sendEmail(config, { to: inviteeEmail, subject, html }).catch(
2132
+ (err) => console.error("[dyrected/core] Failed to send welcome email:", err)
2133
+ );
2134
+ const { password: _, ...safeUser } = user;
2135
+ return c.json({ token: sessionToken, user: safeUser }, 201);
2136
+ }
2137
+ };
2138
+
2139
+ // src/controllers/preview.controller.ts
2140
+ var import_jose2 = require("jose");
2141
+ var import_node_util3 = require("util");
2142
+ var PreviewController = class {
2143
+ getSecret() {
2144
+ const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET || "dyrected-preview-secret-change-me";
2145
+ return new import_node_util3.TextEncoder().encode(secret);
2146
+ }
2147
+ /**
2148
+ * POST /api/preview-token
2149
+ * Generates a short-lived token for previewing unsaved data.
2150
+ */
2151
+ async createToken(c) {
2152
+ const body = await c.req.json().catch(() => null);
2153
+ if (!body?.collectionSlug || !body?.data) {
2154
+ return c.json({ error: true, message: "collectionSlug and data are required." }, 400);
2155
+ }
2156
+ const token = await new import_jose2.SignJWT({
2157
+ collectionSlug: body.collectionSlug,
2158
+ documentId: body.documentId,
2159
+ data: body.data
2160
+ }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime("15m").sign(this.getSecret());
2161
+ const expiresAt = new Date(Date.now() + 15 * 60 * 1e3).toISOString();
2162
+ return c.json({ token, expiresAt });
2163
+ }
2164
+ /**
2165
+ * GET /api/preview-data?token=<jwt>
2166
+ * Returns the data stored in the preview token.
2167
+ */
2168
+ async getData(c) {
2169
+ const token = c.req.query("token");
2170
+ if (!token) {
2171
+ return c.json({ error: true, message: "token query parameter is required." }, 400);
2172
+ }
2173
+ try {
2174
+ const { payload } = await (0, import_jose2.jwtVerify)(token, this.getSecret());
2175
+ return c.json(payload);
2176
+ } catch (err) {
2177
+ return c.json({ error: true, message: "Invalid or expired preview token." }, 401);
2178
+ }
2179
+ }
2180
+ };
2181
+
2182
+ // src/middleware/auth.ts
2183
+ function requireAuth() {
2184
+ return async (c, next) => {
2185
+ const authHeader = c.req.header("Authorization");
2186
+ const token = authHeader?.replace(/^Bearer\s+/i, "");
2187
+ if (!token) {
2188
+ return c.json({ error: true, message: "Authentication required." }, 401);
2189
+ }
2190
+ try {
2191
+ const user = await verifyCollectionToken(token);
2192
+ c.set("user", user);
2193
+ await next();
2194
+ } catch {
2195
+ return c.json({ error: true, message: "Invalid or expired token." }, 401);
2196
+ }
2197
+ };
2198
+ }
2199
+ function optionalAuth() {
2200
+ return async (c, next) => {
2201
+ const authHeader = c.req.header("Authorization");
2202
+ const token = authHeader?.replace(/^Bearer\s+/i, "");
2203
+ if (token) {
2204
+ try {
2205
+ const user = await verifyCollectionToken(token);
2206
+ c.set("user", user);
2207
+ } catch {
2208
+ }
2209
+ }
2210
+ await next();
2211
+ };
2212
+ }
2213
+
2214
+ // src/utils/openapi.ts
2215
+ function generateOpenApi(config) {
2216
+ const spec = {
2217
+ openapi: "3.0.0",
2218
+ info: {
2219
+ title: "Dyrected API",
2220
+ version: "1.0.0",
2221
+ description: "Automatically generated OpenAPI specification for the Dyrected project."
2222
+ },
2223
+ components: {
2224
+ schemas: {},
2225
+ securitySchemes: {
2226
+ ApiKeyAuth: {
2227
+ type: "apiKey",
2228
+ in: "header",
2229
+ name: "x-api-key"
2230
+ }
2231
+ }
2232
+ },
2233
+ paths: {},
2234
+ security: [{ ApiKeyAuth: [] }]
2235
+ };
2236
+ for (const collection of config.collections) {
2237
+ spec.components.schemas[collection.slug] = collectionToSchema(collection);
2238
+ }
2239
+ for (const global of config.globals) {
2240
+ spec.components.schemas[global.slug] = globalToSchema(global);
2241
+ }
2242
+ for (const collection of config.collections) {
2243
+ const slug = collection.slug;
2244
+ const path = `/api/collections/${slug}`;
2245
+ const labels = collection.labels || { singular: slug, plural: `${slug}s` };
2246
+ spec.paths[path] = {
2247
+ get: {
2248
+ tags: ["Collections"],
2249
+ summary: `Find ${labels.plural}`,
2250
+ parameters: [
2251
+ { name: "limit", in: "query", schema: { type: "integer", default: 10 } },
2252
+ { name: "page", in: "query", schema: { type: "integer", default: 1 } },
2253
+ { name: "where", in: "query", schema: { type: "string" }, description: "JSON filter" },
2254
+ { name: "sort", in: "query", schema: { type: "string" }, description: "Sort field (e.g. -createdAt)" }
2255
+ ],
2256
+ responses: {
2257
+ 200: {
2258
+ description: "Success",
2259
+ content: {
2260
+ "application/json": {
2261
+ schema: {
2262
+ type: "object",
2263
+ properties: {
2264
+ docs: { type: "array", items: { $ref: `#/components/schemas/${slug}` } },
2265
+ total: { type: "integer" },
2266
+ limit: { type: "integer" },
2267
+ page: { type: "integer" }
2268
+ }
2269
+ }
2270
+ }
2271
+ }
2272
+ }
2273
+ }
2274
+ },
2275
+ post: {
2276
+ tags: ["Collections"],
2277
+ summary: `Create ${labels.singular}`,
2278
+ requestBody: {
2279
+ required: true,
2280
+ content: {
2281
+ "application/json": {
2282
+ schema: { $ref: `#/components/schemas/${slug}` }
2283
+ }
2284
+ }
2285
+ },
2286
+ responses: {
2287
+ 201: {
2288
+ description: "Created",
2289
+ content: {
2290
+ "application/json": {
2291
+ schema: { $ref: `#/components/schemas/${slug}` }
2292
+ }
2293
+ }
2294
+ }
2295
+ }
2296
+ }
2297
+ };
2298
+ spec.paths[`${path}/{id}`] = {
2299
+ get: {
2300
+ tags: ["Collections"],
2301
+ summary: `Get a single ${labels.singular}`,
2302
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
2303
+ responses: {
2304
+ 200: {
2305
+ description: "Success",
2306
+ content: {
2307
+ "application/json": {
2308
+ schema: { $ref: `#/components/schemas/${slug}` }
2309
+ }
2310
+ }
2311
+ }
2312
+ }
2313
+ },
2314
+ patch: {
2315
+ tags: ["Collections"],
2316
+ summary: `Update ${labels.singular}`,
2317
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
2318
+ requestBody: {
2319
+ required: true,
2320
+ content: {
2321
+ "application/json": {
2322
+ schema: { $ref: `#/components/schemas/${slug}` }
2323
+ }
2324
+ }
2325
+ },
2326
+ responses: {
2327
+ 200: {
2328
+ description: "Updated",
2329
+ content: {
2330
+ "application/json": {
2331
+ schema: { $ref: `#/components/schemas/${slug}` }
2332
+ }
2333
+ }
2334
+ }
2335
+ }
2336
+ },
2337
+ delete: {
2338
+ tags: ["Collections"],
2339
+ summary: `Delete ${labels.singular}`,
2340
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
2341
+ responses: {
2342
+ 204: { description: "Deleted" }
2343
+ }
2344
+ }
2345
+ };
2346
+ }
2347
+ for (const global of config.globals) {
2348
+ const slug = global.slug;
2349
+ const path = `/api/globals/${slug}`;
2350
+ spec.paths[path] = {
2351
+ get: {
2352
+ tags: ["Globals"],
2353
+ summary: `Get ${global.label || slug}`,
2354
+ responses: {
2355
+ 200: {
2356
+ description: "Success",
2357
+ content: {
2358
+ "application/json": {
2359
+ schema: { $ref: `#/components/schemas/${slug}` }
2360
+ }
2361
+ }
2362
+ }
2363
+ }
2364
+ },
2365
+ patch: {
2366
+ tags: ["Globals"],
2367
+ summary: `Update ${global.label || slug}`,
2368
+ requestBody: {
2369
+ required: true,
2370
+ content: {
2371
+ "application/json": {
2372
+ schema: { $ref: `#/components/schemas/${slug}` }
2373
+ }
2374
+ }
2375
+ },
2376
+ responses: {
2377
+ 200: {
2378
+ description: "Updated",
2379
+ content: {
2380
+ "application/json": {
2381
+ schema: { $ref: `#/components/schemas/${slug}` }
2382
+ }
2383
+ }
2384
+ }
2385
+ }
2386
+ }
2387
+ };
2388
+ }
2389
+ if (config.storage) {
2390
+ spec.paths["/api/media"] = {
2391
+ get: {
2392
+ tags: ["Media"],
2393
+ summary: "List Media",
2394
+ responses: {
2395
+ 200: {
2396
+ description: "Success",
2397
+ content: {
2398
+ "application/json": {
2399
+ schema: {
2400
+ type: "object",
2401
+ properties: {
2402
+ docs: { type: "array", items: { type: "object", additionalProperties: true } }
2403
+ }
2404
+ }
2405
+ }
2406
+ }
2407
+ }
2408
+ }
2409
+ },
2410
+ post: {
2411
+ tags: ["Media"],
2412
+ summary: "Upload Media",
2413
+ requestBody: {
2414
+ content: {
2415
+ "multipart/form-data": {
2416
+ schema: {
2417
+ type: "object",
2418
+ properties: {
2419
+ file: { type: "string", format: "binary" }
2420
+ }
2421
+ }
2422
+ }
2423
+ }
2424
+ },
2425
+ responses: {
2426
+ 201: { description: "Uploaded" }
2427
+ }
2428
+ }
2429
+ };
2430
+ }
2431
+ return spec;
2432
+ }
2433
+ function collectionToSchema(collection) {
2434
+ const { properties, required } = fieldsToProperties(collection.fields);
2435
+ return {
2436
+ type: "object",
2437
+ properties: {
2438
+ id: { type: "string" },
2439
+ createdAt: { type: "string", format: "date-time" },
2440
+ updatedAt: { type: "string", format: "date-time" },
2441
+ ...properties
2442
+ },
2443
+ required: ["id", ...required]
2444
+ };
2445
+ }
2446
+ function globalToSchema(global) {
2447
+ const { properties, required } = fieldsToProperties(global.fields);
2448
+ return {
2449
+ type: "object",
2450
+ properties,
2451
+ required
2452
+ };
2453
+ }
2454
+ function fieldsToProperties(fields) {
2455
+ const props = {};
2456
+ const required = [];
2457
+ for (const field of fields) {
2458
+ props[field.name] = fieldToSchema(field);
2459
+ if (field.required) {
2460
+ required.push(field.name);
2461
+ }
2462
+ }
2463
+ return { properties: props, required };
2464
+ }
2465
+ function fieldToSchema(field) {
2466
+ let schema = {};
2467
+ switch (field.type) {
2468
+ case "text":
2469
+ case "textarea":
2470
+ case "email":
2471
+ case "url":
2472
+ schema = { type: "string" };
2473
+ break;
2474
+ case "number":
2475
+ schema = { type: "number" };
2476
+ break;
2477
+ case "boolean":
2478
+ schema = { type: "boolean" };
2479
+ break;
2480
+ case "date":
2481
+ schema = { type: "string", format: "date-time" };
2482
+ break;
2483
+ case "select":
2484
+ schema = { type: "string", enum: field.options?.map((o) => typeof o === "string" ? o : o.value) };
2485
+ break;
2486
+ case "multiSelect":
2487
+ schema = {
2488
+ type: "array",
2489
+ items: { type: "string", enum: field.options?.map((o) => typeof o === "string" ? o : o.value) }
2490
+ };
2491
+ break;
2492
+ case "relationship":
2493
+ schema = { type: "string", description: `ID of a ${field.relationTo} record` };
2494
+ break;
2495
+ case "object": {
2496
+ const { properties, required } = fieldsToProperties(field.fields || []);
2497
+ schema = { type: "object", properties, required };
2498
+ break;
2499
+ }
2500
+ case "array": {
2501
+ const { properties, required } = fieldsToProperties(field.fields || []);
2502
+ schema = { type: "array", items: { type: "object", properties, required } };
2503
+ break;
2504
+ }
2505
+ case "json":
2506
+ case "richText":
2507
+ schema = { type: "object", additionalProperties: true };
2508
+ break;
2509
+ case "blocks":
2510
+ schema = {
2511
+ type: "array",
2512
+ items: {
2513
+ oneOf: field.blocks?.map((block) => {
2514
+ const { properties, required } = fieldsToProperties(block.fields);
2515
+ return {
2516
+ type: "object",
2517
+ properties: {
2518
+ blockType: { type: "string", enum: [block.slug] },
2519
+ ...properties
2520
+ },
2521
+ required: ["blockType", ...required]
2522
+ };
2523
+ })
2524
+ }
2525
+ };
2526
+ break;
2527
+ default:
2528
+ schema = { type: "string" };
2529
+ }
2530
+ if (field.label) schema.description = field.label;
2531
+ return schema;
2532
+ }
2533
+
2534
+ // src/utils/swagger.ts
2535
+ function getSwaggerHtml(specUrl = "/api/openapi.json") {
2536
+ return `
2537
+ <!DOCTYPE html>
2538
+ <html lang="en">
2539
+ <head>
2540
+ <meta charset="utf-8" />
2541
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2542
+ <meta name="description" content="SwaggerUI" />
2543
+ <title>Dyrected API Documentation</title>
2544
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
2545
+ </head>
2546
+ <body>
2547
+ <div id="swagger-ui"></div>
2548
+ <script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" charset="UTF-8"></script>
2549
+ <script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-standalone-preset.js" charset="UTF-8"></script>
2550
+ <script>
2551
+ window.onload = () => {
2552
+ // Forward the apikey query param when loading the spec and making API calls
2553
+ const params = new URLSearchParams(window.location.search);
2554
+ const apiKey = params.get('apikey');
2555
+ const specUrlWithKey = apiKey ? '${specUrl}?apikey=' + encodeURIComponent(apiKey) : '${specUrl}';
2556
+
2557
+ window.ui = SwaggerUIBundle({
2558
+ url: specUrlWithKey,
2559
+ dom_id: '#swagger-ui',
2560
+ presets: [
2561
+ SwaggerUIBundle.presets.apis,
2562
+ SwaggerUIStandalonePreset
2563
+ ],
2564
+ layout: "BaseLayout",
2565
+ deepLinking: true,
2566
+ showExtensions: true,
2567
+ showCommonExtensions: true,
2568
+ // Inject x-api-key header on every request made from the Swagger UI
2569
+ requestInterceptor: (request) => {
2570
+ if (apiKey) {
2571
+ request.headers['x-api-key'] = apiKey;
2572
+ }
2573
+ return request;
2574
+ }
2575
+ });
2576
+ };
2577
+ </script>
2578
+ </body>
2579
+ </html>
2580
+ `;
2581
+ }
2582
+
2583
+ // src/auth/jexl.ts
2584
+ var import_jexl = __toESM(require("jexl"), 1);
2585
+ async function evaluateAccess(expression, context) {
2586
+ if (expression === void 0 || expression === null) return false;
2587
+ if (typeof expression === "boolean") return expression;
2588
+ try {
2589
+ const result = await import_jexl.default.eval(expression, context);
2590
+ return !!result;
2591
+ } catch (err) {
2592
+ console.error("[dyrected/core] Jexl evaluation failed:", err);
2593
+ return false;
2594
+ }
2595
+ }
2596
+
2597
+ // src/router.ts
2598
+ function accessGate(target, action) {
2599
+ return async (c, next) => {
2600
+ const user = c.get("user");
2601
+ const accessExpr = target.access?.[action];
2602
+ if (accessExpr === void 0 || accessExpr === null) {
2603
+ return await next();
2604
+ }
2605
+ const accessArgs = { user, req: c.req, doc: null };
2606
+ const allowed = await evaluateAccess(accessExpr, accessArgs);
2607
+ if (!allowed) {
2608
+ return c.json({ error: true, message: `Access denied: ${action} on ${target.slug}` }, 403);
2609
+ }
2610
+ await next();
2611
+ };
2612
+ }
2613
+ function registerRoutes(app, config) {
2614
+ app.get("/api/schemas", optionalAuth(), async (c) => {
2615
+ const siteId = c.req.header("X-Site-Id");
2616
+ let collections = [...config.collections];
2617
+ let globals = [...config.globals];
2618
+ if (siteId && config.onSchemaFetch) {
2619
+ const dynamic = await config.onSchemaFetch(siteId);
2620
+ if (dynamic.collections) collections = [...collections, ...dynamic.collections];
2621
+ if (dynamic.globals) globals = [...globals, ...dynamic.globals];
2622
+ if (dynamic.admin) {
2623
+ config.admin = { ...config.admin, ...dynamic.admin };
2624
+ }
2625
+ }
2626
+ const user = c.get("user");
2627
+ const accessArgs = { user, req: c.req, doc: null };
2628
+ const resolveAccess = async (access) => {
2629
+ if (access === void 0 || access === null) return true;
2630
+ if (typeof access === "function") {
2631
+ try {
2632
+ const result = await access(accessArgs);
2633
+ return typeof result === "boolean" ? result : !!result;
2634
+ } catch (err) {
2635
+ console.error("[dyrected/core] Functional access check failed:", err);
2636
+ return false;
2637
+ }
2638
+ }
2639
+ if (typeof access === "string" || typeof access === "boolean") {
2640
+ return evaluateAccess(access, accessArgs);
2641
+ }
2642
+ return true;
2643
+ };
2644
+ const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
2645
+ slug: col.slug,
2646
+ labels: col.labels,
2647
+ access: {
2648
+ read: await resolveAccess(col.access?.read),
2649
+ create: await resolveAccess(col.access?.create),
2650
+ update: await resolveAccess(col.access?.update),
2651
+ delete: await resolveAccess(col.access?.delete)
2652
+ },
2653
+ fields: await Promise.all(col.fields.map(async (f) => ({
2654
+ name: f.name,
2655
+ type: f.type,
2656
+ label: f.label,
2657
+ required: f.required,
2658
+ defaultValue: f.defaultValue,
2659
+ options: f.options,
2660
+ relationTo: f.relationTo,
2661
+ hasMany: f.hasMany,
2662
+ fields: f.fields,
2663
+ blocks: f.blocks,
2664
+ admin: f.admin,
2665
+ access: {
2666
+ read: await resolveAccess(f.access?.read),
2667
+ update: await resolveAccess(f.access?.update)
2668
+ }
2669
+ }))),
2670
+ upload: !!col.upload,
2671
+ auth: !!col.auth,
2672
+ admin: col.admin
2673
+ })));
2674
+ const filteredGlobals = await Promise.all(globals.filter((glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId).map(async (glb) => ({
2675
+ slug: glb.slug,
2676
+ label: glb.label,
2677
+ access: {
2678
+ read: await resolveAccess(glb.access?.read),
2679
+ update: await resolveAccess(glb.access?.update)
2680
+ },
2681
+ fields: await Promise.all(glb.fields.map(async (f) => ({
2682
+ name: f.name,
2683
+ type: f.type,
2684
+ label: f.label,
2685
+ required: f.required,
2686
+ defaultValue: f.defaultValue,
2687
+ options: f.options,
2688
+ relationTo: f.relationTo,
2689
+ hasMany: f.hasMany,
2690
+ fields: f.fields,
2691
+ blocks: f.blocks,
2692
+ admin: f.admin,
2693
+ access: {
2694
+ read: await resolveAccess(f.access?.read),
2695
+ update: await resolveAccess(f.access?.update)
2696
+ }
2697
+ }))),
2698
+ admin: glb.admin
2699
+ })));
2700
+ return c.json({
2701
+ collections: filteredCollections,
2702
+ globals: filteredGlobals,
2703
+ admin: config.admin || {}
2704
+ });
2705
+ });
2706
+ app.get("/api/openapi.json", (c) => {
2707
+ return c.json(generateOpenApi(config));
2708
+ });
2709
+ app.get("/api/docs", (c) => {
2710
+ return c.html(getSwaggerHtml());
2711
+ });
2712
+ app.get("/api/media/:filename{.+$}", async (c) => {
2713
+ const mediaController = new MediaController("media");
2714
+ return mediaController.serve(c);
2715
+ });
2716
+ app.get("/media/:filename{.+$}", async (c) => {
2717
+ const mediaController = new MediaController("media");
2718
+ return mediaController.serve(c);
2719
+ });
2720
+ if (config.storage) {
2721
+ const uploadCollections = config.collections.filter((c) => c.upload);
2722
+ for (const col of uploadCollections) {
2723
+ const mediaController = new MediaController(col.slug);
2724
+ const prefix = `/api/collections/${col.slug}`;
2725
+ app.get(`${prefix}/media`, accessGate(col, "read"), (c) => mediaController.find(c));
2726
+ app.get(`${prefix}/media/:filename{.+$}`, (c) => mediaController.serve(c));
2727
+ app.post(`${prefix}/media`, accessGate(col, "create"), (c) => mediaController.upload(c));
2728
+ app.delete(`${prefix}/media/:id`, accessGate(col, "delete"), (c) => mediaController.delete(c));
2729
+ }
2730
+ }
2731
+ for (const collection of config.collections) {
2732
+ if (!collection.auth) continue;
2733
+ const path = `/api/collections/${collection.slug}`;
2734
+ const authController = new AuthController(collection);
2735
+ app.post(`${path}/login`, (c) => authController.login(c));
2736
+ app.post(`${path}/logout`, (c) => authController.logout(c));
2737
+ app.get(`${path}/init`, (c) => authController.init(c));
2738
+ app.post(`${path}/first-user`, (c) => authController.registerFirstUser(c));
2739
+ app.get(`${path}/me`, requireAuth(), (c) => authController.me(c));
2740
+ app.post(`${path}/refresh-token`, requireAuth(), (c) => authController.refreshToken(c));
2741
+ app.post(`${path}/forgot-password`, (c) => authController.forgotPassword(c));
2742
+ app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
2743
+ app.post(`${path}/invite`, requireAuth(), (c) => authController.invite(c));
2744
+ app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
2745
+ }
2746
+ for (const collection of config.collections) {
2747
+ const path = `/api/collections/${collection.slug}`;
2748
+ const controller = new CollectionController(collection);
2749
+ app.get(path, accessGate(collection, "read"), (c) => controller.find(c));
2750
+ app.post(path, accessGate(collection, "create"), (c) => controller.create(c));
2751
+ app.post(`${path}/media`, accessGate(collection, "create"), (c) => controller.create(c));
2752
+ app.delete(`${path}/delete-many`, accessGate(collection, "delete"), (c) => controller.deleteMany(c));
2753
+ app.get(`${path}/:id`, accessGate(collection, "read"), (c) => controller.findOne(c));
2754
+ app.patch(`${path}/:id`, accessGate(collection, "update"), (c) => controller.update(c));
2755
+ app.delete(`${path}/:id`, accessGate(collection, "delete"), (c) => controller.delete(c));
2756
+ app.post(`${path}/seed`, (c) => controller.seed(c));
2757
+ }
2758
+ for (const global of config.globals) {
2759
+ const path = `/api/globals/${global.slug}`;
2760
+ const controller = new GlobalController(global);
2761
+ app.get(path, accessGate(global, "read"), (c) => controller.get(c));
2762
+ app.patch(path, accessGate(global, "update"), (c) => controller.update(c));
2763
+ app.post(`${path}/seed`, (c) => controller.seed(c));
2764
+ }
2765
+ const previewController = new PreviewController();
2766
+ app.post("/api/preview-token", requireAuth(), (c) => previewController.createToken(c));
2767
+ app.get("/api/preview-data", (c) => previewController.getData(c));
2768
+ app.all("/api/collections/:slug/:id?", async (c) => {
2769
+ const slug = c.req.param("slug");
2770
+ const id = c.req.param("id");
2771
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
2772
+ const config2 = c.get("config");
2773
+ if (config2.collections.some((col) => col.slug === slug)) {
2774
+ return c.json({ message: "Method Not Allowed" }, 405);
2775
+ }
2776
+ if (config2.onSchemaFetch && siteId) {
2777
+ const dynamic = await config2.onSchemaFetch(siteId);
2778
+ let collection = dynamic.collections?.find((col) => col.slug === slug);
2779
+ if (!collection && slug === "media") {
2780
+ collection = {
2781
+ slug: "media",
2782
+ labels: { singular: "Media", plural: "Media" },
2783
+ upload: true,
2784
+ fields: []
2785
+ };
2786
+ }
2787
+ if (collection) {
2788
+ if (collection.auth && id) {
2789
+ const authController = new AuthController(collection);
2790
+ const method2 = c.req.method;
2791
+ if (method2 === "POST" && id === "login") return authController.login(c);
2792
+ if (method2 === "POST" && id === "logout") return authController.logout(c);
2793
+ if (method2 === "GET" && id === "me") return authController.me(c);
2794
+ if (method2 === "POST" && id === "refresh-token") return authController.refreshToken(c);
2795
+ if (method2 === "POST" && id === "forgot-password") return authController.forgotPassword(c);
2796
+ if (method2 === "POST" && id === "reset-password") return authController.resetPassword(c);
2797
+ }
2798
+ const controller = new CollectionController(collection);
2799
+ const method = c.req.method;
2800
+ if (id) {
2801
+ if (method === "GET") return controller.findOne(c);
2802
+ if (method === "PATCH") return controller.update(c);
2803
+ if (method === "DELETE" && id === "delete-many") return controller.deleteMany(c);
2804
+ if (method === "DELETE") return controller.delete(c);
2805
+ if (method === "POST" && id === "media") return controller.create(c);
2806
+ if (method === "POST" && id === "seed") return controller.seed(c);
2807
+ } else {
2808
+ if (method === "GET") return controller.find(c);
2809
+ if (method === "POST") return controller.create(c);
2810
+ }
2811
+ }
2812
+ }
2813
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
2814
+ });
2815
+ app.all("/api/globals/:slug", async (c) => {
2816
+ const slug = c.req.param("slug");
2817
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
2818
+ const config2 = c.get("config");
2819
+ if (config2.globals.some((glb) => glb.slug === slug)) {
2820
+ return c.json({ message: "Method Not Allowed" }, 405);
2821
+ }
2822
+ if (config2.onSchemaFetch && siteId) {
2823
+ const dynamic = await config2.onSchemaFetch(siteId);
2824
+ const global = dynamic.globals?.find((glb) => glb.slug === slug);
2825
+ if (global) {
2826
+ const controller = new GlobalController(global);
2827
+ if (c.req.method === "GET") return controller.get(c);
2828
+ if (c.req.method === "PATCH") return controller.update(c);
2829
+ }
2830
+ }
2831
+ return c.json({ message: `Global "${slug}" not found` }, 404);
2832
+ });
2833
+ }
2834
+
2835
+ // src/app.ts
2836
+ async function createDyrectedApp(rawConfig) {
2837
+ const config = normalizeConfig(rawConfig);
2838
+ const app = new import_hono.Hono();
2839
+ if (config.db?.sync) {
2840
+ await config.db.sync(config.collections, config.globals);
2841
+ }
2842
+ app.use("*", (0, import_request_id.requestId)());
2843
+ app.use("*", optionalAuth());
2844
+ app.use("*", async (c, next) => {
2845
+ const start = Date.now();
2846
+ await next();
2847
+ const ms = Date.now() - start;
2848
+ console.log(`[dyrected/api] ${c.req.method} ${c.req.path} ${c.res.status} - ${ms}ms`);
2849
+ });
2850
+ app.use("*", (0, import_cors.cors)());
2851
+ app.use("*", async (c, next) => {
2852
+ c.set("config", config);
2853
+ if (!c.get("siteId")) {
2854
+ c.set("siteId", "default");
2855
+ }
2856
+ await next();
2857
+ });
2858
+ app.get("/health", (c) => c.json({ status: "ok", version: "0.0.1" }));
2859
+ app.get("/routes", (c) => {
2860
+ const routes = app.routes.map((r) => ({ method: r.method, path: r.path }));
2861
+ return c.json({ routes });
2862
+ });
2863
+ app.onError((err, c) => {
2864
+ console.error(`[dyrected/core] Uncaught Error:`, err);
2865
+ return c.json({
2866
+ message: err.message || "Internal Server Error",
2867
+ stack: process.env.NODE_ENV === "development" ? err.stack : void 0
2868
+ }, 500);
2869
+ });
2870
+ registerRoutes(app, config);
2871
+ return app;
2872
+ }
2873
+
543
2874
  // src/index.ts
544
2875
  function defineCollection(config) {
545
2876
  return config;
@@ -552,9 +2883,13 @@ function defineConfig(config) {
552
2883
  }
553
2884
  // Annotate the CommonJS export names for ESM import in node:
554
2885
  0 && (module.exports = {
2886
+ createDyrectedApp,
555
2887
  defineCollection,
556
2888
  defineConfig,
557
2889
  defineGlobal,
558
2890
  generateAIPrompt,
559
- normalizeConfig
2891
+ generateFreshSetupPrompt,
2892
+ normalizeConfig,
2893
+ parseMongoWhere,
2894
+ parseSqlWhere
560
2895
  });