@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 +3 -0
- package/dist/index.d.mts +265 -0
- package/dist/index.d.ts +265 -0
- package/dist/index.js +391 -0
- package/dist/index.mjs +359 -0
- package/package.json +26 -0
package/README.md
ADDED
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|