@dyrected/core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @dyrected/core
2
+
3
+ This is the core package/app for the Dyrected ecosystem.
@@ -0,0 +1,265 @@
1
+ import * as hono_types from 'hono/types';
2
+ import { Hono } from 'hono';
3
+
4
+ type FieldType = 'text' | 'textarea' | 'richText' | 'number' | 'boolean' | 'date' | 'select' | 'multiSelect' | 'email' | 'url' | 'relationship' | 'array' | 'object' | 'json' | 'blocks';
5
+ interface Block {
6
+ slug: string;
7
+ labels?: {
8
+ singular: string;
9
+ plural: string;
10
+ };
11
+ fields: Field[];
12
+ }
13
+ interface Field {
14
+ name: string;
15
+ type: FieldType;
16
+ label?: string;
17
+ required?: boolean;
18
+ unique?: boolean;
19
+ defaultValue?: any;
20
+ options?: string[] | {
21
+ label: string;
22
+ value: string;
23
+ }[];
24
+ collection?: string;
25
+ fields?: Field[];
26
+ blocks?: Block[];
27
+ access?: {
28
+ read?: AccessFunction;
29
+ update?: AccessFunction;
30
+ };
31
+ hooks?: {
32
+ beforeChange?: FieldHook[];
33
+ afterRead?: FieldHook[];
34
+ };
35
+ admin?: {
36
+ placeholder?: string;
37
+ description?: string;
38
+ hidden?: boolean;
39
+ readOnly?: boolean;
40
+ condition?: (data: any) => boolean;
41
+ };
42
+ }
43
+ type AccessFunction = (args: {
44
+ user: any;
45
+ doc?: any;
46
+ data?: any;
47
+ req: any;
48
+ }) => boolean | object | Promise<boolean | object>;
49
+ type HookFunction = (args: {
50
+ data?: any;
51
+ doc?: any;
52
+ user?: any;
53
+ req?: any;
54
+ }) => any | Promise<any>;
55
+ type FieldHook = (args: {
56
+ value: any;
57
+ originalDoc?: any;
58
+ data?: any;
59
+ user?: any;
60
+ }) => any | Promise<any>;
61
+ interface CollectionConfig {
62
+ slug: string;
63
+ labels?: {
64
+ singular: string;
65
+ plural: string;
66
+ };
67
+ auth?: boolean;
68
+ upload?: boolean | UploadConfig;
69
+ fields: Field[];
70
+ access?: {
71
+ read?: AccessFunction;
72
+ create?: AccessFunction;
73
+ update?: AccessFunction;
74
+ delete?: AccessFunction;
75
+ };
76
+ hooks?: {
77
+ beforeRead?: HookFunction[];
78
+ afterRead?: HookFunction[];
79
+ beforeChange?: HookFunction[];
80
+ afterChange?: HookFunction[];
81
+ beforeDelete?: HookFunction[];
82
+ afterDelete?: HookFunction[];
83
+ };
84
+ admin?: {
85
+ useAsTitle?: string;
86
+ defaultColumns?: string[];
87
+ group?: string;
88
+ hidden?: boolean;
89
+ };
90
+ }
91
+ interface UploadConfig {
92
+ allowedMimeTypes?: string[];
93
+ maxFileSize?: number;
94
+ imageSizes?: {
95
+ name: string;
96
+ width: number;
97
+ height: number;
98
+ crop?: string;
99
+ }[];
100
+ }
101
+ interface GlobalConfig {
102
+ slug: string;
103
+ label?: string;
104
+ fields: Field[];
105
+ access?: {
106
+ read?: AccessFunction;
107
+ update?: AccessFunction;
108
+ };
109
+ hooks?: {
110
+ beforeRead?: HookFunction[];
111
+ afterRead?: HookFunction[];
112
+ beforeChange?: HookFunction[];
113
+ afterChange?: HookFunction[];
114
+ };
115
+ admin?: {
116
+ group?: string;
117
+ hidden?: boolean;
118
+ };
119
+ }
120
+ interface PaginatedResult<T = any> {
121
+ docs: T[];
122
+ total: number;
123
+ limit: number;
124
+ page: number;
125
+ }
126
+ interface DatabaseAdapter {
127
+ find(args: {
128
+ collection: string;
129
+ where?: any;
130
+ limit?: number;
131
+ page?: number;
132
+ sort?: string;
133
+ }): Promise<PaginatedResult>;
134
+ findOne(args: {
135
+ collection: string;
136
+ id: string;
137
+ }): Promise<any>;
138
+ create(args: {
139
+ collection: string;
140
+ data: any;
141
+ }): Promise<any>;
142
+ update(args: {
143
+ collection: string;
144
+ id: string;
145
+ data: any;
146
+ }): Promise<any>;
147
+ delete(args: {
148
+ collection: string;
149
+ id: string;
150
+ }): Promise<any>;
151
+ getGlobal(args: {
152
+ slug: string;
153
+ }): Promise<any>;
154
+ updateGlobal(args: {
155
+ slug: string;
156
+ data: any;
157
+ }): Promise<any>;
158
+ }
159
+ interface FileData {
160
+ filename: string;
161
+ filesize?: number;
162
+ mimeType: string;
163
+ url: string;
164
+ width?: number;
165
+ height?: number;
166
+ type?: 'upload' | 'external';
167
+ provider?: string;
168
+ provider_metadata?: any;
169
+ [key: string]: any;
170
+ }
171
+ interface StorageAdapter {
172
+ upload(args: {
173
+ filename: string;
174
+ buffer: Buffer;
175
+ mimeType: string;
176
+ }): Promise<FileData>;
177
+ delete(args: {
178
+ filename: string;
179
+ }): Promise<void>;
180
+ getURL(args: {
181
+ filename: string;
182
+ }): string;
183
+ }
184
+ interface DyrectedConfig {
185
+ collections: CollectionConfig[];
186
+ globals: GlobalConfig[];
187
+ db: DatabaseAdapter;
188
+ storage?: StorageAdapter;
189
+ email?: {
190
+ provider: string;
191
+ apiKey?: string;
192
+ from: string;
193
+ };
194
+ redis?: {
195
+ url: string;
196
+ };
197
+ cors?: {
198
+ origins: string[];
199
+ };
200
+ }
201
+
202
+ interface DyrectedContext {
203
+ Variables: {
204
+ config: DyrectedConfig;
205
+ siteId?: string;
206
+ };
207
+ }
208
+ /**
209
+ * Create the main Dyrected Hono application.
210
+ */
211
+ declare function createDyrectedApp(config: DyrectedConfig): Hono<DyrectedContext, hono_types.BlankSchema, "/">;
212
+
213
+ declare class PopulationService {
214
+ private db;
215
+ private collections;
216
+ constructor(db: DatabaseAdapter, collections: CollectionConfig[]);
217
+ /**
218
+ * Recursively populate relationship fields in a document or array of documents.
219
+ */
220
+ populate(args: {
221
+ data: any;
222
+ fields: Field[];
223
+ currentDepth: number;
224
+ maxDepth: number;
225
+ }): Promise<any>;
226
+ /**
227
+ * Helper to populate a PaginatedResult
228
+ */
229
+ populateResult(result: PaginatedResult, fields: Field[], maxDepth: number): Promise<PaginatedResult>;
230
+ }
231
+
232
+ /**
233
+ * MediaService handles background tasks for media assets,
234
+ * such as extracting metadata from external URLs (YouTube/Vimeo).
235
+ */
236
+ declare class MediaService {
237
+ /**
238
+ * Fetches metadata for a given URL.
239
+ * Supports YouTube and Vimeo.
240
+ */
241
+ static fetchMetadata(url: string): Promise<{
242
+ provider: string;
243
+ provider_id: string;
244
+ thumbnail: string;
245
+ embedUrl: string;
246
+ type: "video";
247
+ } | null>;
248
+ private static extractYoutubeId;
249
+ private static extractVimeoId;
250
+ }
251
+
252
+ /**
253
+ * Define a collection configuration with full type safety.
254
+ */
255
+ declare function defineCollection(config: CollectionConfig): CollectionConfig;
256
+ /**
257
+ * Define a global configuration with full type safety.
258
+ */
259
+ declare function defineGlobal(config: GlobalConfig): GlobalConfig;
260
+ /**
261
+ * Define the main Dyrected configuration.
262
+ */
263
+ declare function defineConfig(config: DyrectedConfig): DyrectedConfig;
264
+
265
+ export { type AccessFunction, type Block, type CollectionConfig, type DatabaseAdapter, type DyrectedConfig, type DyrectedContext, type Field, type FieldHook, type FieldType, type FileData, type GlobalConfig, type HookFunction, MediaService, type PaginatedResult, PopulationService, type StorageAdapter, type UploadConfig, createDyrectedApp, defineCollection, defineConfig, defineGlobal };
@@ -0,0 +1,265 @@
1
+ import * as hono_types from 'hono/types';
2
+ import { Hono } from 'hono';
3
+
4
+ type FieldType = 'text' | 'textarea' | 'richText' | 'number' | 'boolean' | 'date' | 'select' | 'multiSelect' | 'email' | 'url' | 'relationship' | 'array' | 'object' | 'json' | 'blocks';
5
+ interface Block {
6
+ slug: string;
7
+ labels?: {
8
+ singular: string;
9
+ plural: string;
10
+ };
11
+ fields: Field[];
12
+ }
13
+ interface Field {
14
+ name: string;
15
+ type: FieldType;
16
+ label?: string;
17
+ required?: boolean;
18
+ unique?: boolean;
19
+ defaultValue?: any;
20
+ options?: string[] | {
21
+ label: string;
22
+ value: string;
23
+ }[];
24
+ collection?: string;
25
+ fields?: Field[];
26
+ blocks?: Block[];
27
+ access?: {
28
+ read?: AccessFunction;
29
+ update?: AccessFunction;
30
+ };
31
+ hooks?: {
32
+ beforeChange?: FieldHook[];
33
+ afterRead?: FieldHook[];
34
+ };
35
+ admin?: {
36
+ placeholder?: string;
37
+ description?: string;
38
+ hidden?: boolean;
39
+ readOnly?: boolean;
40
+ condition?: (data: any) => boolean;
41
+ };
42
+ }
43
+ type AccessFunction = (args: {
44
+ user: any;
45
+ doc?: any;
46
+ data?: any;
47
+ req: any;
48
+ }) => boolean | object | Promise<boolean | object>;
49
+ type HookFunction = (args: {
50
+ data?: any;
51
+ doc?: any;
52
+ user?: any;
53
+ req?: any;
54
+ }) => any | Promise<any>;
55
+ type FieldHook = (args: {
56
+ value: any;
57
+ originalDoc?: any;
58
+ data?: any;
59
+ user?: any;
60
+ }) => any | Promise<any>;
61
+ interface CollectionConfig {
62
+ slug: string;
63
+ labels?: {
64
+ singular: string;
65
+ plural: string;
66
+ };
67
+ auth?: boolean;
68
+ upload?: boolean | UploadConfig;
69
+ fields: Field[];
70
+ access?: {
71
+ read?: AccessFunction;
72
+ create?: AccessFunction;
73
+ update?: AccessFunction;
74
+ delete?: AccessFunction;
75
+ };
76
+ hooks?: {
77
+ beforeRead?: HookFunction[];
78
+ afterRead?: HookFunction[];
79
+ beforeChange?: HookFunction[];
80
+ afterChange?: HookFunction[];
81
+ beforeDelete?: HookFunction[];
82
+ afterDelete?: HookFunction[];
83
+ };
84
+ admin?: {
85
+ useAsTitle?: string;
86
+ defaultColumns?: string[];
87
+ group?: string;
88
+ hidden?: boolean;
89
+ };
90
+ }
91
+ interface UploadConfig {
92
+ allowedMimeTypes?: string[];
93
+ maxFileSize?: number;
94
+ imageSizes?: {
95
+ name: string;
96
+ width: number;
97
+ height: number;
98
+ crop?: string;
99
+ }[];
100
+ }
101
+ interface GlobalConfig {
102
+ slug: string;
103
+ label?: string;
104
+ fields: Field[];
105
+ access?: {
106
+ read?: AccessFunction;
107
+ update?: AccessFunction;
108
+ };
109
+ hooks?: {
110
+ beforeRead?: HookFunction[];
111
+ afterRead?: HookFunction[];
112
+ beforeChange?: HookFunction[];
113
+ afterChange?: HookFunction[];
114
+ };
115
+ admin?: {
116
+ group?: string;
117
+ hidden?: boolean;
118
+ };
119
+ }
120
+ interface PaginatedResult<T = any> {
121
+ docs: T[];
122
+ total: number;
123
+ limit: number;
124
+ page: number;
125
+ }
126
+ interface DatabaseAdapter {
127
+ find(args: {
128
+ collection: string;
129
+ where?: any;
130
+ limit?: number;
131
+ page?: number;
132
+ sort?: string;
133
+ }): Promise<PaginatedResult>;
134
+ findOne(args: {
135
+ collection: string;
136
+ id: string;
137
+ }): Promise<any>;
138
+ create(args: {
139
+ collection: string;
140
+ data: any;
141
+ }): Promise<any>;
142
+ update(args: {
143
+ collection: string;
144
+ id: string;
145
+ data: any;
146
+ }): Promise<any>;
147
+ delete(args: {
148
+ collection: string;
149
+ id: string;
150
+ }): Promise<any>;
151
+ getGlobal(args: {
152
+ slug: string;
153
+ }): Promise<any>;
154
+ updateGlobal(args: {
155
+ slug: string;
156
+ data: any;
157
+ }): Promise<any>;
158
+ }
159
+ interface FileData {
160
+ filename: string;
161
+ filesize?: number;
162
+ mimeType: string;
163
+ url: string;
164
+ width?: number;
165
+ height?: number;
166
+ type?: 'upload' | 'external';
167
+ provider?: string;
168
+ provider_metadata?: any;
169
+ [key: string]: any;
170
+ }
171
+ interface StorageAdapter {
172
+ upload(args: {
173
+ filename: string;
174
+ buffer: Buffer;
175
+ mimeType: string;
176
+ }): Promise<FileData>;
177
+ delete(args: {
178
+ filename: string;
179
+ }): Promise<void>;
180
+ getURL(args: {
181
+ filename: string;
182
+ }): string;
183
+ }
184
+ interface DyrectedConfig {
185
+ collections: CollectionConfig[];
186
+ globals: GlobalConfig[];
187
+ db: DatabaseAdapter;
188
+ storage?: StorageAdapter;
189
+ email?: {
190
+ provider: string;
191
+ apiKey?: string;
192
+ from: string;
193
+ };
194
+ redis?: {
195
+ url: string;
196
+ };
197
+ cors?: {
198
+ origins: string[];
199
+ };
200
+ }
201
+
202
+ interface DyrectedContext {
203
+ Variables: {
204
+ config: DyrectedConfig;
205
+ siteId?: string;
206
+ };
207
+ }
208
+ /**
209
+ * Create the main Dyrected Hono application.
210
+ */
211
+ declare function createDyrectedApp(config: DyrectedConfig): Hono<DyrectedContext, hono_types.BlankSchema, "/">;
212
+
213
+ declare class PopulationService {
214
+ private db;
215
+ private collections;
216
+ constructor(db: DatabaseAdapter, collections: CollectionConfig[]);
217
+ /**
218
+ * Recursively populate relationship fields in a document or array of documents.
219
+ */
220
+ populate(args: {
221
+ data: any;
222
+ fields: Field[];
223
+ currentDepth: number;
224
+ maxDepth: number;
225
+ }): Promise<any>;
226
+ /**
227
+ * Helper to populate a PaginatedResult
228
+ */
229
+ populateResult(result: PaginatedResult, fields: Field[], maxDepth: number): Promise<PaginatedResult>;
230
+ }
231
+
232
+ /**
233
+ * MediaService handles background tasks for media assets,
234
+ * such as extracting metadata from external URLs (YouTube/Vimeo).
235
+ */
236
+ declare class MediaService {
237
+ /**
238
+ * Fetches metadata for a given URL.
239
+ * Supports YouTube and Vimeo.
240
+ */
241
+ static fetchMetadata(url: string): Promise<{
242
+ provider: string;
243
+ provider_id: string;
244
+ thumbnail: string;
245
+ embedUrl: string;
246
+ type: "video";
247
+ } | null>;
248
+ private static extractYoutubeId;
249
+ private static extractVimeoId;
250
+ }
251
+
252
+ /**
253
+ * Define a collection configuration with full type safety.
254
+ */
255
+ declare function defineCollection(config: CollectionConfig): CollectionConfig;
256
+ /**
257
+ * Define a global configuration with full type safety.
258
+ */
259
+ declare function defineGlobal(config: GlobalConfig): GlobalConfig;
260
+ /**
261
+ * Define the main Dyrected configuration.
262
+ */
263
+ declare function defineConfig(config: DyrectedConfig): DyrectedConfig;
264
+
265
+ export { type AccessFunction, type Block, type CollectionConfig, type DatabaseAdapter, type DyrectedConfig, type DyrectedContext, type Field, type FieldHook, type FieldType, type FileData, type GlobalConfig, type HookFunction, MediaService, type PaginatedResult, PopulationService, type StorageAdapter, type UploadConfig, createDyrectedApp, defineCollection, defineConfig, defineGlobal };
package/dist/index.js ADDED
@@ -0,0 +1,391 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ MediaService: () => MediaService,
24
+ PopulationService: () => PopulationService,
25
+ createDyrectedApp: () => createDyrectedApp,
26
+ defineCollection: () => defineCollection,
27
+ defineConfig: () => defineConfig,
28
+ defineGlobal: () => defineGlobal
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/app.ts
33
+ var import_hono = require("hono");
34
+ var import_logger = require("hono/logger");
35
+ var import_cors = require("hono/cors");
36
+ var import_request_id = require("hono/request-id");
37
+
38
+ // src/services/population.service.ts
39
+ var PopulationService = class {
40
+ constructor(db, collections) {
41
+ this.db = db;
42
+ this.collections = collections;
43
+ }
44
+ db;
45
+ collections;
46
+ /**
47
+ * Recursively populate relationship fields in a document or array of documents.
48
+ */
49
+ async populate(args) {
50
+ const { data, fields, currentDepth, maxDepth } = args;
51
+ if (currentDepth >= maxDepth || !data) {
52
+ return data;
53
+ }
54
+ if (Array.isArray(data)) {
55
+ return Promise.all(data.map((item) => this.populate({ ...args, data: item })));
56
+ }
57
+ const populatedDoc = { ...data };
58
+ for (const field of fields) {
59
+ const value = populatedDoc[field.name];
60
+ if (field.type === "relationship" && field.collection && value) {
61
+ const relatedCollection = this.collections.find((c) => c.slug === field.collection);
62
+ if (!relatedCollection) continue;
63
+ if (Array.isArray(value)) {
64
+ populatedDoc[field.name] = await Promise.all(
65
+ value.map(async (id) => {
66
+ const doc = await this.db.findOne({ collection: field.collection, id });
67
+ if (!doc) return id;
68
+ return this.populate({
69
+ data: doc,
70
+ fields: relatedCollection.fields,
71
+ currentDepth: currentDepth + 1,
72
+ maxDepth
73
+ });
74
+ })
75
+ );
76
+ } else if (typeof value === "string") {
77
+ const doc = await this.db.findOne({ collection: field.collection, id: value });
78
+ if (doc) {
79
+ populatedDoc[field.name] = await this.populate({
80
+ data: doc,
81
+ fields: relatedCollection.fields,
82
+ currentDepth: currentDepth + 1,
83
+ maxDepth
84
+ });
85
+ }
86
+ }
87
+ }
88
+ if ((field.type === "array" || field.type === "object") && field.fields && value) {
89
+ populatedDoc[field.name] = await this.populate({
90
+ data: value,
91
+ fields: field.fields,
92
+ currentDepth,
93
+ // Nested fields don't consume depth, only relationships do
94
+ maxDepth
95
+ });
96
+ }
97
+ }
98
+ return populatedDoc;
99
+ }
100
+ /**
101
+ * Helper to populate a PaginatedResult
102
+ */
103
+ async populateResult(result, fields, maxDepth) {
104
+ if (maxDepth <= 0) return result;
105
+ const populatedDocs = await this.populate({
106
+ data: result.docs,
107
+ fields,
108
+ currentDepth: 0,
109
+ maxDepth
110
+ });
111
+ return {
112
+ ...result,
113
+ docs: populatedDocs
114
+ };
115
+ }
116
+ };
117
+
118
+ // src/controllers/collection.controller.ts
119
+ var CollectionController = class {
120
+ constructor(collection) {
121
+ this.collection = collection;
122
+ }
123
+ collection;
124
+ async find(c) {
125
+ const config = c.get("config");
126
+ const db = config.db;
127
+ const limit = Number(c.req.query("limit")) || 10;
128
+ const page = Number(c.req.query("page")) || 1;
129
+ const depth = Number(c.req.query("depth")) || 0;
130
+ let result = await db.find({
131
+ collection: this.collection.slug,
132
+ limit,
133
+ page
134
+ });
135
+ if (depth > 0) {
136
+ const populationService = new PopulationService(db, config.collections);
137
+ result = await populationService.populateResult(result, this.collection.fields, depth);
138
+ }
139
+ return c.json(result);
140
+ }
141
+ async findOne(c) {
142
+ const config = c.get("config");
143
+ const db = config.db;
144
+ const id = c.req.param("id");
145
+ const depth = Number(c.req.query("depth")) || 0;
146
+ if (!id) return c.json({ message: "Missing ID" }, 400);
147
+ const doc = await db.findOne({ collection: this.collection.slug, id });
148
+ if (!doc) return c.json({ message: "Not Found" }, 404);
149
+ if (depth > 0 && doc) {
150
+ const populationService = new PopulationService(db, config.collections);
151
+ const populatedDoc = await populationService.populate({
152
+ data: doc,
153
+ fields: this.collection.fields,
154
+ currentDepth: 0,
155
+ maxDepth: depth
156
+ });
157
+ return c.json(populatedDoc);
158
+ }
159
+ return c.json(doc);
160
+ }
161
+ async create(c) {
162
+ const db = c.get("config").db;
163
+ const body = await c.req.json();
164
+ const doc = await db.create({ collection: this.collection.slug, data: body });
165
+ return c.json(doc, 201);
166
+ }
167
+ async update(c) {
168
+ const db = c.get("config").db;
169
+ const id = c.req.param("id");
170
+ if (!id) return c.json({ message: "Missing ID" }, 400);
171
+ const body = await c.req.json();
172
+ const doc = await db.update({ collection: this.collection.slug, id, data: body });
173
+ return c.json(doc);
174
+ }
175
+ async delete(c) {
176
+ const db = c.get("config").db;
177
+ const id = c.req.param("id");
178
+ if (!id) return c.json({ message: "Missing ID" }, 400);
179
+ await db.delete({ collection: this.collection.slug, id });
180
+ return c.json({ message: "Deleted" });
181
+ }
182
+ };
183
+
184
+ // src/controllers/global.controller.ts
185
+ var GlobalController = class {
186
+ constructor(global) {
187
+ this.global = global;
188
+ }
189
+ global;
190
+ async get(c) {
191
+ const config = c.get("config");
192
+ const db = config.db;
193
+ const depth = Number(c.req.query("depth")) || 0;
194
+ const data = await db.getGlobal({ slug: this.global.slug });
195
+ if (depth > 0 && data) {
196
+ const populationService = new PopulationService(db, config.collections);
197
+ const populatedData = await populationService.populate({
198
+ data,
199
+ fields: this.global.fields,
200
+ currentDepth: 0,
201
+ maxDepth: depth
202
+ });
203
+ return c.json(populatedData);
204
+ }
205
+ return c.json(data || {});
206
+ }
207
+ async update(c) {
208
+ const db = c.get("config").db;
209
+ const body = await c.req.json();
210
+ const data = await db.updateGlobal({ slug: this.global.slug, data: body });
211
+ return c.json(data);
212
+ }
213
+ };
214
+
215
+ // src/controllers/media.controller.ts
216
+ var MediaController = class {
217
+ async upload(c) {
218
+ const config = c.get("config");
219
+ const storage = config.storage;
220
+ if (!storage) {
221
+ return c.json({ message: "Storage not configured" }, 500);
222
+ }
223
+ const body = await c.req.parseBody();
224
+ const file = body["file"];
225
+ if (!file) {
226
+ return c.json({ message: "No file uploaded" }, 400);
227
+ }
228
+ const buffer = Buffer.from(await file.arrayBuffer());
229
+ const fileData = await storage.upload({
230
+ filename: file.name,
231
+ buffer,
232
+ mimeType: file.type
233
+ });
234
+ const db = config.db;
235
+ const doc = await db.create({
236
+ collection: "media",
237
+ data: fileData
238
+ });
239
+ return c.json(doc, 201);
240
+ }
241
+ async find(c) {
242
+ const db = c.get("config").db;
243
+ const limit = Number(c.req.query("limit")) || 10;
244
+ const page = Number(c.req.query("page")) || 1;
245
+ const result = await db.find({
246
+ collection: "media",
247
+ limit,
248
+ page
249
+ });
250
+ return c.json(result);
251
+ }
252
+ async delete(c) {
253
+ const config = c.get("config");
254
+ const storage = config.storage;
255
+ const db = config.db;
256
+ const id = c.req.param("id");
257
+ if (!id) return c.json({ message: "Missing ID" }, 400);
258
+ const doc = await db.findOne({ collection: "media", id });
259
+ if (!doc) return c.json({ message: "Not Found" }, 404);
260
+ if (storage) {
261
+ await storage.delete({ filename: doc.filename });
262
+ }
263
+ await db.delete({ collection: "media", id });
264
+ return c.json({ message: "Deleted" });
265
+ }
266
+ };
267
+
268
+ // src/router.ts
269
+ function registerRoutes(app, config) {
270
+ app.get("/api/schemas", (c) => {
271
+ return c.json({
272
+ collections: config.collections.map((col) => ({
273
+ slug: col.slug,
274
+ labels: col.labels,
275
+ fields: col.fields,
276
+ auth: col.auth,
277
+ upload: col.upload
278
+ })),
279
+ globals: config.globals.map((glb) => ({
280
+ slug: glb.slug,
281
+ label: glb.label,
282
+ fields: glb.fields
283
+ }))
284
+ });
285
+ });
286
+ if (config.storage) {
287
+ const mediaController = new MediaController();
288
+ app.get("/api/media", (c) => mediaController.find(c));
289
+ app.post("/api/media", (c) => mediaController.upload(c));
290
+ app.delete("/api/media/:id", (c) => mediaController.delete(c));
291
+ }
292
+ for (const collection of config.collections) {
293
+ const path = `/api/collections/${collection.slug}`;
294
+ const controller = new CollectionController(collection);
295
+ app.get(path, (c) => controller.find(c));
296
+ app.post(path, (c) => controller.create(c));
297
+ app.get(`${path}/:id`, (c) => controller.findOne(c));
298
+ app.patch(`${path}/:id`, (c) => controller.update(c));
299
+ app.delete(`${path}/:id`, (c) => controller.delete(c));
300
+ }
301
+ for (const global of config.globals) {
302
+ const path = `/api/globals/${global.slug}`;
303
+ const controller = new GlobalController(global);
304
+ app.get(path, (c) => controller.get(c));
305
+ app.patch(path, (c) => controller.update(c));
306
+ }
307
+ }
308
+
309
+ // src/app.ts
310
+ function createDyrectedApp(config) {
311
+ const app = new import_hono.Hono();
312
+ app.use("*", (0, import_request_id.requestId)());
313
+ app.use("*", (0, import_logger.logger)());
314
+ app.use("*", (0, import_cors.cors)());
315
+ app.use("*", async (c, next) => {
316
+ c.set("config", config);
317
+ if (!c.get("siteId")) {
318
+ c.set("siteId", "default");
319
+ }
320
+ await next();
321
+ });
322
+ app.get("/health", (c) => c.json({ status: "ok", version: "0.0.1" }));
323
+ registerRoutes(app, config);
324
+ return app;
325
+ }
326
+
327
+ // src/services/media.service.ts
328
+ var MediaService = class {
329
+ /**
330
+ * Fetches metadata for a given URL.
331
+ * Supports YouTube and Vimeo.
332
+ */
333
+ static async fetchMetadata(url) {
334
+ if (!url) return null;
335
+ if (url.includes("youtube.com") || url.includes("youtu.be")) {
336
+ const videoId = this.extractYoutubeId(url);
337
+ if (videoId) {
338
+ return {
339
+ provider: "youtube",
340
+ provider_id: videoId,
341
+ thumbnail: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
342
+ embedUrl: `https://www.youtube.com/embed/${videoId}`,
343
+ type: "video"
344
+ };
345
+ }
346
+ }
347
+ if (url.includes("vimeo.com")) {
348
+ const vimeoId = this.extractVimeoId(url);
349
+ if (vimeoId) {
350
+ return {
351
+ provider: "vimeo",
352
+ provider_id: vimeoId,
353
+ thumbnail: "",
354
+ // Requires oEmbed API for reliable thumbnails
355
+ embedUrl: `https://player.vimeo.com/video/${vimeoId}`,
356
+ type: "video"
357
+ };
358
+ }
359
+ }
360
+ return null;
361
+ }
362
+ static extractYoutubeId(url) {
363
+ const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
364
+ const match = url.match(regExp);
365
+ return match && match[2].length === 11 ? match[2] : null;
366
+ }
367
+ static extractVimeoId(url) {
368
+ const match = url.match(/vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|album\/(?:\d+)\/video\/|video\/|)(\d+)(?:$|\/|\?)/);
369
+ return match ? match[1] : null;
370
+ }
371
+ };
372
+
373
+ // src/index.ts
374
+ function defineCollection(config) {
375
+ return config;
376
+ }
377
+ function defineGlobal(config) {
378
+ return config;
379
+ }
380
+ function defineConfig(config) {
381
+ return config;
382
+ }
383
+ // Annotate the CommonJS export names for ESM import in node:
384
+ 0 && (module.exports = {
385
+ MediaService,
386
+ PopulationService,
387
+ createDyrectedApp,
388
+ defineCollection,
389
+ defineConfig,
390
+ defineGlobal
391
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,359 @@
1
+ // src/app.ts
2
+ import { Hono } from "hono";
3
+ import { logger } from "hono/logger";
4
+ import { cors } from "hono/cors";
5
+ import { requestId } from "hono/request-id";
6
+
7
+ // src/services/population.service.ts
8
+ var PopulationService = class {
9
+ constructor(db, collections) {
10
+ this.db = db;
11
+ this.collections = collections;
12
+ }
13
+ db;
14
+ collections;
15
+ /**
16
+ * Recursively populate relationship fields in a document or array of documents.
17
+ */
18
+ async populate(args) {
19
+ const { data, fields, currentDepth, maxDepth } = args;
20
+ if (currentDepth >= maxDepth || !data) {
21
+ return data;
22
+ }
23
+ if (Array.isArray(data)) {
24
+ return Promise.all(data.map((item) => this.populate({ ...args, data: item })));
25
+ }
26
+ const populatedDoc = { ...data };
27
+ for (const field of fields) {
28
+ const value = populatedDoc[field.name];
29
+ if (field.type === "relationship" && field.collection && value) {
30
+ const relatedCollection = this.collections.find((c) => c.slug === field.collection);
31
+ if (!relatedCollection) continue;
32
+ if (Array.isArray(value)) {
33
+ populatedDoc[field.name] = await Promise.all(
34
+ value.map(async (id) => {
35
+ const doc = await this.db.findOne({ collection: field.collection, id });
36
+ if (!doc) return id;
37
+ return this.populate({
38
+ data: doc,
39
+ fields: relatedCollection.fields,
40
+ currentDepth: currentDepth + 1,
41
+ maxDepth
42
+ });
43
+ })
44
+ );
45
+ } else if (typeof value === "string") {
46
+ const doc = await this.db.findOne({ collection: field.collection, id: value });
47
+ if (doc) {
48
+ populatedDoc[field.name] = await this.populate({
49
+ data: doc,
50
+ fields: relatedCollection.fields,
51
+ currentDepth: currentDepth + 1,
52
+ maxDepth
53
+ });
54
+ }
55
+ }
56
+ }
57
+ if ((field.type === "array" || field.type === "object") && field.fields && value) {
58
+ populatedDoc[field.name] = await this.populate({
59
+ data: value,
60
+ fields: field.fields,
61
+ currentDepth,
62
+ // Nested fields don't consume depth, only relationships do
63
+ maxDepth
64
+ });
65
+ }
66
+ }
67
+ return populatedDoc;
68
+ }
69
+ /**
70
+ * Helper to populate a PaginatedResult
71
+ */
72
+ async populateResult(result, fields, maxDepth) {
73
+ if (maxDepth <= 0) return result;
74
+ const populatedDocs = await this.populate({
75
+ data: result.docs,
76
+ fields,
77
+ currentDepth: 0,
78
+ maxDepth
79
+ });
80
+ return {
81
+ ...result,
82
+ docs: populatedDocs
83
+ };
84
+ }
85
+ };
86
+
87
+ // src/controllers/collection.controller.ts
88
+ var CollectionController = class {
89
+ constructor(collection) {
90
+ this.collection = collection;
91
+ }
92
+ collection;
93
+ async find(c) {
94
+ const config = c.get("config");
95
+ const db = config.db;
96
+ const limit = Number(c.req.query("limit")) || 10;
97
+ const page = Number(c.req.query("page")) || 1;
98
+ const depth = Number(c.req.query("depth")) || 0;
99
+ let result = await db.find({
100
+ collection: this.collection.slug,
101
+ limit,
102
+ page
103
+ });
104
+ if (depth > 0) {
105
+ const populationService = new PopulationService(db, config.collections);
106
+ result = await populationService.populateResult(result, this.collection.fields, depth);
107
+ }
108
+ return c.json(result);
109
+ }
110
+ async findOne(c) {
111
+ const config = c.get("config");
112
+ const db = config.db;
113
+ const id = c.req.param("id");
114
+ const depth = Number(c.req.query("depth")) || 0;
115
+ if (!id) return c.json({ message: "Missing ID" }, 400);
116
+ const doc = await db.findOne({ collection: this.collection.slug, id });
117
+ if (!doc) return c.json({ message: "Not Found" }, 404);
118
+ if (depth > 0 && doc) {
119
+ const populationService = new PopulationService(db, config.collections);
120
+ const populatedDoc = await populationService.populate({
121
+ data: doc,
122
+ fields: this.collection.fields,
123
+ currentDepth: 0,
124
+ maxDepth: depth
125
+ });
126
+ return c.json(populatedDoc);
127
+ }
128
+ return c.json(doc);
129
+ }
130
+ async create(c) {
131
+ const db = c.get("config").db;
132
+ const body = await c.req.json();
133
+ const doc = await db.create({ collection: this.collection.slug, data: body });
134
+ return c.json(doc, 201);
135
+ }
136
+ async update(c) {
137
+ const db = c.get("config").db;
138
+ const id = c.req.param("id");
139
+ if (!id) return c.json({ message: "Missing ID" }, 400);
140
+ const body = await c.req.json();
141
+ const doc = await db.update({ collection: this.collection.slug, id, data: body });
142
+ return c.json(doc);
143
+ }
144
+ async delete(c) {
145
+ const db = c.get("config").db;
146
+ const id = c.req.param("id");
147
+ if (!id) return c.json({ message: "Missing ID" }, 400);
148
+ await db.delete({ collection: this.collection.slug, id });
149
+ return c.json({ message: "Deleted" });
150
+ }
151
+ };
152
+
153
+ // src/controllers/global.controller.ts
154
+ var GlobalController = class {
155
+ constructor(global) {
156
+ this.global = global;
157
+ }
158
+ global;
159
+ async get(c) {
160
+ const config = c.get("config");
161
+ const db = config.db;
162
+ const depth = Number(c.req.query("depth")) || 0;
163
+ const data = await db.getGlobal({ slug: this.global.slug });
164
+ if (depth > 0 && data) {
165
+ const populationService = new PopulationService(db, config.collections);
166
+ const populatedData = await populationService.populate({
167
+ data,
168
+ fields: this.global.fields,
169
+ currentDepth: 0,
170
+ maxDepth: depth
171
+ });
172
+ return c.json(populatedData);
173
+ }
174
+ return c.json(data || {});
175
+ }
176
+ async update(c) {
177
+ const db = c.get("config").db;
178
+ const body = await c.req.json();
179
+ const data = await db.updateGlobal({ slug: this.global.slug, data: body });
180
+ return c.json(data);
181
+ }
182
+ };
183
+
184
+ // src/controllers/media.controller.ts
185
+ var MediaController = class {
186
+ async upload(c) {
187
+ const config = c.get("config");
188
+ const storage = config.storage;
189
+ if (!storage) {
190
+ return c.json({ message: "Storage not configured" }, 500);
191
+ }
192
+ const body = await c.req.parseBody();
193
+ const file = body["file"];
194
+ if (!file) {
195
+ return c.json({ message: "No file uploaded" }, 400);
196
+ }
197
+ const buffer = Buffer.from(await file.arrayBuffer());
198
+ const fileData = await storage.upload({
199
+ filename: file.name,
200
+ buffer,
201
+ mimeType: file.type
202
+ });
203
+ const db = config.db;
204
+ const doc = await db.create({
205
+ collection: "media",
206
+ data: fileData
207
+ });
208
+ return c.json(doc, 201);
209
+ }
210
+ async find(c) {
211
+ const db = c.get("config").db;
212
+ const limit = Number(c.req.query("limit")) || 10;
213
+ const page = Number(c.req.query("page")) || 1;
214
+ const result = await db.find({
215
+ collection: "media",
216
+ limit,
217
+ page
218
+ });
219
+ return c.json(result);
220
+ }
221
+ async delete(c) {
222
+ const config = c.get("config");
223
+ const storage = config.storage;
224
+ const db = config.db;
225
+ const id = c.req.param("id");
226
+ if (!id) return c.json({ message: "Missing ID" }, 400);
227
+ const doc = await db.findOne({ collection: "media", id });
228
+ if (!doc) return c.json({ message: "Not Found" }, 404);
229
+ if (storage) {
230
+ await storage.delete({ filename: doc.filename });
231
+ }
232
+ await db.delete({ collection: "media", id });
233
+ return c.json({ message: "Deleted" });
234
+ }
235
+ };
236
+
237
+ // src/router.ts
238
+ function registerRoutes(app, config) {
239
+ app.get("/api/schemas", (c) => {
240
+ return c.json({
241
+ collections: config.collections.map((col) => ({
242
+ slug: col.slug,
243
+ labels: col.labels,
244
+ fields: col.fields,
245
+ auth: col.auth,
246
+ upload: col.upload
247
+ })),
248
+ globals: config.globals.map((glb) => ({
249
+ slug: glb.slug,
250
+ label: glb.label,
251
+ fields: glb.fields
252
+ }))
253
+ });
254
+ });
255
+ if (config.storage) {
256
+ const mediaController = new MediaController();
257
+ app.get("/api/media", (c) => mediaController.find(c));
258
+ app.post("/api/media", (c) => mediaController.upload(c));
259
+ app.delete("/api/media/:id", (c) => mediaController.delete(c));
260
+ }
261
+ for (const collection of config.collections) {
262
+ const path = `/api/collections/${collection.slug}`;
263
+ const controller = new CollectionController(collection);
264
+ app.get(path, (c) => controller.find(c));
265
+ app.post(path, (c) => controller.create(c));
266
+ app.get(`${path}/:id`, (c) => controller.findOne(c));
267
+ app.patch(`${path}/:id`, (c) => controller.update(c));
268
+ app.delete(`${path}/:id`, (c) => controller.delete(c));
269
+ }
270
+ for (const global of config.globals) {
271
+ const path = `/api/globals/${global.slug}`;
272
+ const controller = new GlobalController(global);
273
+ app.get(path, (c) => controller.get(c));
274
+ app.patch(path, (c) => controller.update(c));
275
+ }
276
+ }
277
+
278
+ // src/app.ts
279
+ function createDyrectedApp(config) {
280
+ const app = new Hono();
281
+ app.use("*", requestId());
282
+ app.use("*", logger());
283
+ app.use("*", cors());
284
+ app.use("*", async (c, next) => {
285
+ c.set("config", config);
286
+ if (!c.get("siteId")) {
287
+ c.set("siteId", "default");
288
+ }
289
+ await next();
290
+ });
291
+ app.get("/health", (c) => c.json({ status: "ok", version: "0.0.1" }));
292
+ registerRoutes(app, config);
293
+ return app;
294
+ }
295
+
296
+ // src/services/media.service.ts
297
+ var MediaService = class {
298
+ /**
299
+ * Fetches metadata for a given URL.
300
+ * Supports YouTube and Vimeo.
301
+ */
302
+ static async fetchMetadata(url) {
303
+ if (!url) return null;
304
+ if (url.includes("youtube.com") || url.includes("youtu.be")) {
305
+ const videoId = this.extractYoutubeId(url);
306
+ if (videoId) {
307
+ return {
308
+ provider: "youtube",
309
+ provider_id: videoId,
310
+ thumbnail: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
311
+ embedUrl: `https://www.youtube.com/embed/${videoId}`,
312
+ type: "video"
313
+ };
314
+ }
315
+ }
316
+ if (url.includes("vimeo.com")) {
317
+ const vimeoId = this.extractVimeoId(url);
318
+ if (vimeoId) {
319
+ return {
320
+ provider: "vimeo",
321
+ provider_id: vimeoId,
322
+ thumbnail: "",
323
+ // Requires oEmbed API for reliable thumbnails
324
+ embedUrl: `https://player.vimeo.com/video/${vimeoId}`,
325
+ type: "video"
326
+ };
327
+ }
328
+ }
329
+ return null;
330
+ }
331
+ static extractYoutubeId(url) {
332
+ const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
333
+ const match = url.match(regExp);
334
+ return match && match[2].length === 11 ? match[2] : null;
335
+ }
336
+ static extractVimeoId(url) {
337
+ const match = url.match(/vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|album\/(?:\d+)\/video\/|video\/|)(\d+)(?:$|\/|\?)/);
338
+ return match ? match[1] : null;
339
+ }
340
+ };
341
+
342
+ // src/index.ts
343
+ function defineCollection(config) {
344
+ return config;
345
+ }
346
+ function defineGlobal(config) {
347
+ return config;
348
+ }
349
+ function defineConfig(config) {
350
+ return config;
351
+ }
352
+ export {
353
+ MediaService,
354
+ PopulationService,
355
+ createDyrectedApp,
356
+ defineCollection,
357
+ defineConfig,
358
+ defineGlobal
359
+ };
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@dyrected/core",
3
+ "version": "0.0.1",
4
+ "main": "./dist/index.js",
5
+ "module": "./dist/index.mjs",
6
+ "types": "./dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "dependencies": {
11
+ "hono": "^4.0.0",
12
+ "zod": "^3.22.0"
13
+ },
14
+ "devDependencies": {
15
+ "@types/node": "^20.12.12",
16
+ "tsup": "^8.0.0",
17
+ "typescript": "^5.0.0",
18
+ "vitest": "^1.0.0"
19
+ },
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format cjs,esm --dts",
22
+ "dev": "tsup src/index.ts --format cjs,esm --watch --dts",
23
+ "lint": "eslint src/**/*.ts",
24
+ "test": "vitest"
25
+ }
26
+ }