includio-cms 0.0.34 → 0.0.35
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/admin/components/fields/seo-field.svelte +4 -2
- package/dist/admin/components/media/file-upload.svelte +4 -5
- package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
- package/dist/admin/components/media/media-library.svelte +7 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
- package/dist/core/server/fields/resolveImageFields.js +44 -7
- package/dist/core/server/generator/fields.js +4 -1
- package/dist/core/server/generator/generator.js +1 -1
- package/dist/core/server/media/styles/operations/createMediaStyles.d.ts +3 -0
- package/dist/core/server/media/styles/operations/createMediaStyles.js +7 -0
- package/dist/core/server/media/styles/operations/getImageStyle.d.ts +3 -0
- package/dist/core/server/media/styles/operations/getImageStyle.js +8 -0
- package/dist/core/server/media/styles/sharp/generateImageStyle.d.ts +3 -0
- package/dist/core/server/media/styles/sharp/generateImageStyle.js +16 -0
- package/dist/db-postgres/index.js +30 -0
- package/dist/db-postgres/schema/imageStyle.d.ts +160 -0
- package/dist/db-postgres/schema/imageStyle.js +12 -0
- package/dist/db-postgres/schema/index.d.ts +1 -0
- package/dist/db-postgres/schema/index.js +1 -0
- package/dist/files-local/index.js +12 -25
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -1
- package/dist/sveltekit/components/image.svelte +25 -12
- package/dist/sveltekit/components/image.svelte.d.ts +3 -2
- package/dist/types/adapters/files.d.ts +2 -0
- package/dist/types/adapters.d.ts +6 -1
- package/dist/types/fields.d.ts +14 -0
- package/dist/types/media.d.ts +5 -0
- package/package.json +1 -1
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
type: 'text',
|
|
49
49
|
slug: 'title',
|
|
50
50
|
label: 'Title',
|
|
51
|
-
required: true
|
|
51
|
+
required: true,
|
|
52
|
+
maxLength: 60
|
|
52
53
|
};
|
|
53
54
|
|
|
54
55
|
const descriptionField: TextField = {
|
|
@@ -56,7 +57,8 @@
|
|
|
56
57
|
slug: 'description',
|
|
57
58
|
label: 'Description',
|
|
58
59
|
required: false,
|
|
59
|
-
multiline: true
|
|
60
|
+
multiline: true,
|
|
61
|
+
maxLength: 160
|
|
60
62
|
};
|
|
61
63
|
|
|
62
64
|
const keyWordsField: TextField = {
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { getRemotes } from '../../context/remotes.js';
|
|
3
2
|
import Button from '../../../components/ui/button/button.svelte';
|
|
4
3
|
import Plus from '@tabler/icons-svelte/icons/plus';
|
|
5
4
|
|
|
6
|
-
const remotes = getRemotes();
|
|
7
|
-
|
|
8
5
|
type Props = {
|
|
9
6
|
folderId?: string | null;
|
|
10
7
|
accept?: string;
|
|
8
|
+
onUpload?: () => void;
|
|
11
9
|
};
|
|
12
10
|
|
|
13
|
-
let { folderId = $bindable(null), accept }: Props = $props();
|
|
11
|
+
let { folderId = $bindable(null), accept, onUpload }: Props = $props();
|
|
14
12
|
|
|
15
13
|
let inputElement: HTMLInputElement;
|
|
16
14
|
|
|
@@ -30,10 +28,11 @@
|
|
|
30
28
|
method: 'POST',
|
|
31
29
|
body: form
|
|
32
30
|
});
|
|
31
|
+
|
|
32
|
+
onUpload?.();
|
|
33
33
|
})
|
|
34
34
|
);
|
|
35
35
|
|
|
36
|
-
remotes.getMedia().refresh();
|
|
37
36
|
input.value = '';
|
|
38
37
|
}
|
|
39
38
|
</script>
|
|
@@ -78,7 +78,13 @@
|
|
|
78
78
|
</Tabs.Root>
|
|
79
79
|
</div>
|
|
80
80
|
<div class="flex items-center gap-2">
|
|
81
|
-
<FileUpload
|
|
81
|
+
<FileUpload
|
|
82
|
+
bind:folderId={selectedFolder}
|
|
83
|
+
{accept}
|
|
84
|
+
onUpload={() => {
|
|
85
|
+
fetchFiles(selectedFolder, accept);
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
82
88
|
</div>
|
|
83
89
|
</div>
|
|
84
90
|
|
|
@@ -2,7 +2,7 @@ declare const SidebarInput: import("svelte").Component<(Omit<import("svelte/elem
|
|
|
2
2
|
type: "file";
|
|
3
3
|
files?: FileList;
|
|
4
4
|
} | {
|
|
5
|
-
type?: "number" | "
|
|
5
|
+
type?: "number" | "image" | "text" | "date" | "radio" | "url" | "email" | "checkbox" | "hidden" | "color" | "submit" | "reset" | "button" | (string & {}) | "search" | "tel" | "time" | "datetime-local" | "month" | "password" | "range" | "week";
|
|
6
6
|
files?: undefined;
|
|
7
7
|
})) & {
|
|
8
8
|
ref?: HTMLElement | null | undefined;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PUBLIC_URL } from '$env/static/public';
|
|
2
2
|
import { getCMS } from '../../cms.js';
|
|
3
3
|
import z from 'zod';
|
|
4
|
+
import { getImageStyle } from '../media/styles/operations/getImageStyle.js';
|
|
4
5
|
export async function resolveMediaFields(data, fields) {
|
|
5
6
|
const mediaIds = [];
|
|
6
7
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -60,7 +61,7 @@ export async function resolveMediaFields(data, fields) {
|
|
|
60
61
|
});
|
|
61
62
|
const mediaMap = Object.fromEntries(media.map((m) => [m.id, m]));
|
|
62
63
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
-
const resolveValues = (value, fields) => {
|
|
64
|
+
const resolveValues = async (value, fields) => {
|
|
64
65
|
// Start with a copy of all original data to preserve non-field properties
|
|
65
66
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
67
|
const result = { ...value };
|
|
@@ -71,7 +72,42 @@ export async function resolveMediaFields(data, fields) {
|
|
|
71
72
|
continue;
|
|
72
73
|
}
|
|
73
74
|
switch (field.type) {
|
|
74
|
-
case 'image':
|
|
75
|
+
case 'image': {
|
|
76
|
+
if (field.multiple && Array.isArray(val)) {
|
|
77
|
+
result[field.slug] = val.map((id) => ({
|
|
78
|
+
data: mediaMap[id] ?? null,
|
|
79
|
+
styles: {}
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
if (field.styles) {
|
|
84
|
+
const styles = await Promise.all(field.styles.map(async (style) => {
|
|
85
|
+
const styleDbData = await getImageStyle(val, style);
|
|
86
|
+
return [
|
|
87
|
+
style.name,
|
|
88
|
+
{
|
|
89
|
+
url: styleDbData.url,
|
|
90
|
+
media: style.media,
|
|
91
|
+
mimeType: style.format
|
|
92
|
+
}
|
|
93
|
+
];
|
|
94
|
+
}));
|
|
95
|
+
const finalVal = {
|
|
96
|
+
data: mediaMap[val] ?? null,
|
|
97
|
+
styles: Object.fromEntries(styles)
|
|
98
|
+
};
|
|
99
|
+
result[field.slug] = finalVal;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
const finalVal = {
|
|
103
|
+
data: mediaMap[val] ?? null,
|
|
104
|
+
styles: {}
|
|
105
|
+
};
|
|
106
|
+
result[field.slug] = finalVal;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
75
111
|
case 'file': {
|
|
76
112
|
if (field.multiple && Array.isArray(val)) {
|
|
77
113
|
result[field.slug] = val.map((id) => mediaMap[id] ?? null);
|
|
@@ -98,23 +134,24 @@ export async function resolveMediaFields(data, fields) {
|
|
|
98
134
|
case 'object':
|
|
99
135
|
result[field.slug] = {
|
|
100
136
|
...val,
|
|
101
|
-
data: resolveValues(val.data, field.fields)
|
|
137
|
+
data: await resolveValues(val.data, field.fields)
|
|
102
138
|
};
|
|
103
139
|
break;
|
|
104
140
|
case 'array':
|
|
141
|
+
result[field.slug] = await Promise.all(
|
|
105
142
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
-
|
|
143
|
+
val.map(async (item) => {
|
|
107
144
|
// Find the object definition for this item based on its slug
|
|
108
145
|
const objectDef = field.of.find((objDef) => objDef.slug === item.slug);
|
|
109
146
|
if (objectDef) {
|
|
110
147
|
return {
|
|
111
148
|
...item,
|
|
112
|
-
data: resolveValues(item.data, objectDef.fields)
|
|
149
|
+
data: await resolveValues(item.data, objectDef.fields)
|
|
113
150
|
};
|
|
114
151
|
}
|
|
115
152
|
// If no matching object definition found, return item unchanged
|
|
116
153
|
return item;
|
|
117
|
-
});
|
|
154
|
+
}));
|
|
118
155
|
break;
|
|
119
156
|
default:
|
|
120
157
|
result[field.slug] = val;
|
|
@@ -122,5 +159,5 @@ export async function resolveMediaFields(data, fields) {
|
|
|
122
159
|
}
|
|
123
160
|
return result;
|
|
124
161
|
};
|
|
125
|
-
return resolveValues(data, fields);
|
|
162
|
+
return (await resolveValues(data, fields));
|
|
126
163
|
}
|
|
@@ -7,7 +7,10 @@ function getFieldTypeAsString(field) {
|
|
|
7
7
|
return 'string';
|
|
8
8
|
case 'radio':
|
|
9
9
|
return field.options.map((opt) => `'${opt.value.trim()}'`).join(' | ');
|
|
10
|
-
case 'image':
|
|
10
|
+
case 'image': {
|
|
11
|
+
const base = field.multiple ? 'ImageFieldData[]' : 'ImageFieldData';
|
|
12
|
+
return base + (field.required ? '' : ' | null');
|
|
13
|
+
}
|
|
11
14
|
case 'file': {
|
|
12
15
|
const base = field.multiple ? 'MediaFile[]' : 'MediaFile';
|
|
13
16
|
return base + (field.required ? '' : ' | null');
|
|
@@ -65,7 +65,7 @@ function generateTypes(config) {
|
|
|
65
65
|
const cmsDir = join(process.cwd(), 'src/lib/cms/runtime');
|
|
66
66
|
const filePath = join(cmsDir, 'types.ts');
|
|
67
67
|
let code = `// This file is auto-generated. Do not edit directly.\n\n`;
|
|
68
|
-
code += `import type { MediaFile } from 'includio-cms/types';\n\n`;
|
|
68
|
+
code += `import type { MediaFile, ImageFieldData } from 'includio-cms/types';\n\n`;
|
|
69
69
|
if (config.singles && config.singles.length > 0) {
|
|
70
70
|
code += generateTypesStringForRecords('single', Object.values(config.singles));
|
|
71
71
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getCMS } from '../../../../cms.js';
|
|
2
|
+
import { generateImageStyle } from '../sharp/generateImageStyle.js';
|
|
3
|
+
export async function createImageStyle(mediaFileId, style) {
|
|
4
|
+
const imageStyleFile = await generateImageStyle(mediaFileId, style);
|
|
5
|
+
const imageStyle = await getCMS().databaseAdapter.createImageStyle(mediaFileId, imageStyleFile, style);
|
|
6
|
+
return imageStyle;
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getCMS } from '../../../../cms.js';
|
|
2
|
+
import { createImageStyle } from './createMediaStyles.js';
|
|
3
|
+
export async function getImageStyle(mediaFileId, style) {
|
|
4
|
+
const imageStyle = await getCMS().databaseAdapter.getImageStyle(mediaFileId, style);
|
|
5
|
+
if (imageStyle)
|
|
6
|
+
return imageStyle;
|
|
7
|
+
return createImageStyle(mediaFileId, style);
|
|
8
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getCMS } from '../../../../cms.js';
|
|
2
|
+
import sharp from 'sharp';
|
|
3
|
+
export async function generateImageStyle(mediaFileId, style) {
|
|
4
|
+
const file = await getCMS().filesAdapter.getFile(mediaFileId);
|
|
5
|
+
if (!file) {
|
|
6
|
+
throw new Error('Media file not found');
|
|
7
|
+
}
|
|
8
|
+
const imageBuffer = await file.arrayBuffer();
|
|
9
|
+
const outputBuffer = await sharp(Buffer.from(imageBuffer))
|
|
10
|
+
.resize(style.width, style.height)
|
|
11
|
+
.toFormat(style.format)
|
|
12
|
+
.toBuffer();
|
|
13
|
+
return getCMS().filesAdapter.uploadFile(new File([new Uint8Array(outputBuffer)], `${mediaFileId}_${style.name}.${style.format}`, {
|
|
14
|
+
type: `image/${style.format}`
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
@@ -197,6 +197,36 @@ export function pg(config) {
|
|
|
197
197
|
.returning()
|
|
198
198
|
.then(([file]) => file);
|
|
199
199
|
},
|
|
200
|
+
getImageStyle: async (mediaFileId, style) => {
|
|
201
|
+
const [imageStyle] = await db
|
|
202
|
+
.select()
|
|
203
|
+
.from(schema.imageStylesTable)
|
|
204
|
+
.where(and(eq(schema.imageStylesTable.mediaFileId, mediaFileId), eq(schema.imageStylesTable.name, style.name), eq(schema.imageStylesTable.width, style.width), eq(schema.imageStylesTable.height, style.height)))
|
|
205
|
+
.limit(1);
|
|
206
|
+
if (!imageStyle)
|
|
207
|
+
return null;
|
|
208
|
+
return {
|
|
209
|
+
url: imageStyle.url,
|
|
210
|
+
mimeType: imageStyle.mimeType,
|
|
211
|
+
media: imageStyle.media ? imageStyle.media : undefined
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
createImageStyle: async (mediaFileId, file, style) => {
|
|
215
|
+
const [imageStyle] = await db
|
|
216
|
+
.insert(schema.imageStylesTable)
|
|
217
|
+
.values({
|
|
218
|
+
...style,
|
|
219
|
+
url: file.url,
|
|
220
|
+
mimeType: file.mimeType ?? `image/${style.format}`,
|
|
221
|
+
mediaFileId: mediaFileId
|
|
222
|
+
})
|
|
223
|
+
.returning();
|
|
224
|
+
return {
|
|
225
|
+
url: imageStyle.url,
|
|
226
|
+
mimeType: imageStyle.mimeType,
|
|
227
|
+
media: imageStyle.media ? imageStyle.media : undefined
|
|
228
|
+
};
|
|
229
|
+
},
|
|
200
230
|
deleteMediaFile: async (id) => {
|
|
201
231
|
await db.delete(schema.mediaFilesTable).where(eq(schema.mediaFilesTable.id, id));
|
|
202
232
|
},
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
export declare const imageStylesTable: import("drizzle-orm/pg-core/table", { with: { "resolution-mode": "require" } }).PgTableWithColumns<{
|
|
2
|
+
name: "image_styles";
|
|
3
|
+
schema: undefined;
|
|
4
|
+
columns: {
|
|
5
|
+
id: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
6
|
+
name: "id";
|
|
7
|
+
tableName: "image_styles";
|
|
8
|
+
dataType: "string";
|
|
9
|
+
columnType: "PgUUID";
|
|
10
|
+
data: string;
|
|
11
|
+
driverParam: string;
|
|
12
|
+
notNull: true;
|
|
13
|
+
hasDefault: true;
|
|
14
|
+
isPrimaryKey: true;
|
|
15
|
+
isAutoincrement: false;
|
|
16
|
+
hasRuntimeDefault: false;
|
|
17
|
+
enumValues: undefined;
|
|
18
|
+
baseColumn: never;
|
|
19
|
+
identity: undefined;
|
|
20
|
+
generated: undefined;
|
|
21
|
+
}, {}, {}>;
|
|
22
|
+
mediaFileId: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
23
|
+
name: "media_file_id";
|
|
24
|
+
tableName: "image_styles";
|
|
25
|
+
dataType: "string";
|
|
26
|
+
columnType: "PgUUID";
|
|
27
|
+
data: string;
|
|
28
|
+
driverParam: string;
|
|
29
|
+
notNull: true;
|
|
30
|
+
hasDefault: false;
|
|
31
|
+
isPrimaryKey: false;
|
|
32
|
+
isAutoincrement: false;
|
|
33
|
+
hasRuntimeDefault: false;
|
|
34
|
+
enumValues: undefined;
|
|
35
|
+
baseColumn: never;
|
|
36
|
+
identity: undefined;
|
|
37
|
+
generated: undefined;
|
|
38
|
+
}, {}, {}>;
|
|
39
|
+
name: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
40
|
+
name: "name";
|
|
41
|
+
tableName: "image_styles";
|
|
42
|
+
dataType: "string";
|
|
43
|
+
columnType: "PgText";
|
|
44
|
+
data: string;
|
|
45
|
+
driverParam: string;
|
|
46
|
+
notNull: true;
|
|
47
|
+
hasDefault: false;
|
|
48
|
+
isPrimaryKey: false;
|
|
49
|
+
isAutoincrement: false;
|
|
50
|
+
hasRuntimeDefault: false;
|
|
51
|
+
enumValues: [string, ...string[]];
|
|
52
|
+
baseColumn: never;
|
|
53
|
+
identity: undefined;
|
|
54
|
+
generated: undefined;
|
|
55
|
+
}, {}, {}>;
|
|
56
|
+
url: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
57
|
+
name: "url";
|
|
58
|
+
tableName: "image_styles";
|
|
59
|
+
dataType: "string";
|
|
60
|
+
columnType: "PgText";
|
|
61
|
+
data: string;
|
|
62
|
+
driverParam: string;
|
|
63
|
+
notNull: true;
|
|
64
|
+
hasDefault: false;
|
|
65
|
+
isPrimaryKey: false;
|
|
66
|
+
isAutoincrement: false;
|
|
67
|
+
hasRuntimeDefault: false;
|
|
68
|
+
enumValues: [string, ...string[]];
|
|
69
|
+
baseColumn: never;
|
|
70
|
+
identity: undefined;
|
|
71
|
+
generated: undefined;
|
|
72
|
+
}, {}, {}>;
|
|
73
|
+
width: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
74
|
+
name: "width";
|
|
75
|
+
tableName: "image_styles";
|
|
76
|
+
dataType: "number";
|
|
77
|
+
columnType: "PgInteger";
|
|
78
|
+
data: number;
|
|
79
|
+
driverParam: string | number;
|
|
80
|
+
notNull: true;
|
|
81
|
+
hasDefault: false;
|
|
82
|
+
isPrimaryKey: false;
|
|
83
|
+
isAutoincrement: false;
|
|
84
|
+
hasRuntimeDefault: false;
|
|
85
|
+
enumValues: undefined;
|
|
86
|
+
baseColumn: never;
|
|
87
|
+
identity: undefined;
|
|
88
|
+
generated: undefined;
|
|
89
|
+
}, {}, {}>;
|
|
90
|
+
height: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
91
|
+
name: "height";
|
|
92
|
+
tableName: "image_styles";
|
|
93
|
+
dataType: "number";
|
|
94
|
+
columnType: "PgInteger";
|
|
95
|
+
data: number;
|
|
96
|
+
driverParam: string | number;
|
|
97
|
+
notNull: true;
|
|
98
|
+
hasDefault: false;
|
|
99
|
+
isPrimaryKey: false;
|
|
100
|
+
isAutoincrement: false;
|
|
101
|
+
hasRuntimeDefault: false;
|
|
102
|
+
enumValues: undefined;
|
|
103
|
+
baseColumn: never;
|
|
104
|
+
identity: undefined;
|
|
105
|
+
generated: undefined;
|
|
106
|
+
}, {}, {}>;
|
|
107
|
+
crop: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
108
|
+
name: "crop";
|
|
109
|
+
tableName: "image_styles";
|
|
110
|
+
dataType: "boolean";
|
|
111
|
+
columnType: "PgBoolean";
|
|
112
|
+
data: boolean;
|
|
113
|
+
driverParam: boolean;
|
|
114
|
+
notNull: true;
|
|
115
|
+
hasDefault: true;
|
|
116
|
+
isPrimaryKey: false;
|
|
117
|
+
isAutoincrement: false;
|
|
118
|
+
hasRuntimeDefault: false;
|
|
119
|
+
enumValues: undefined;
|
|
120
|
+
baseColumn: never;
|
|
121
|
+
identity: undefined;
|
|
122
|
+
generated: undefined;
|
|
123
|
+
}, {}, {}>;
|
|
124
|
+
media: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
125
|
+
name: "media";
|
|
126
|
+
tableName: "image_styles";
|
|
127
|
+
dataType: "string";
|
|
128
|
+
columnType: "PgText";
|
|
129
|
+
data: string;
|
|
130
|
+
driverParam: string;
|
|
131
|
+
notNull: false;
|
|
132
|
+
hasDefault: false;
|
|
133
|
+
isPrimaryKey: false;
|
|
134
|
+
isAutoincrement: false;
|
|
135
|
+
hasRuntimeDefault: false;
|
|
136
|
+
enumValues: [string, ...string[]];
|
|
137
|
+
baseColumn: never;
|
|
138
|
+
identity: undefined;
|
|
139
|
+
generated: undefined;
|
|
140
|
+
}, {}, {}>;
|
|
141
|
+
mimeType: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
142
|
+
name: "mime_type";
|
|
143
|
+
tableName: "image_styles";
|
|
144
|
+
dataType: "string";
|
|
145
|
+
columnType: "PgText";
|
|
146
|
+
data: string;
|
|
147
|
+
driverParam: string;
|
|
148
|
+
notNull: true;
|
|
149
|
+
hasDefault: false;
|
|
150
|
+
isPrimaryKey: false;
|
|
151
|
+
isAutoincrement: false;
|
|
152
|
+
hasRuntimeDefault: false;
|
|
153
|
+
enumValues: [string, ...string[]];
|
|
154
|
+
baseColumn: never;
|
|
155
|
+
identity: undefined;
|
|
156
|
+
generated: undefined;
|
|
157
|
+
}, {}, {}>;
|
|
158
|
+
};
|
|
159
|
+
dialect: "pg";
|
|
160
|
+
}>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { boolean, integer, pgTable, text, uuid } from 'drizzle-orm/pg-core';
|
|
2
|
+
export const imageStylesTable = pgTable('image_styles', {
|
|
3
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
4
|
+
mediaFileId: uuid('media_file_id').notNull(),
|
|
5
|
+
name: text('name').notNull(),
|
|
6
|
+
url: text('url').notNull(),
|
|
7
|
+
width: integer('width').notNull(),
|
|
8
|
+
height: integer('height').notNull(),
|
|
9
|
+
crop: boolean('crop').notNull().default(false),
|
|
10
|
+
media: text('media'),
|
|
11
|
+
mimeType: text('mime_type').notNull()
|
|
12
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { extname, resolve } from 'node:path';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { writeFile, readFile } from 'node:fs/promises';
|
|
4
4
|
import sharp from 'sharp';
|
|
5
5
|
import ffmpeg from 'fluent-ffmpeg';
|
|
6
6
|
const fullDir = process.env.NODE_ENV === 'production' ? `/data/uploads` : `./static/uploads`;
|
|
@@ -53,30 +53,17 @@ async function processVideo(filepath, filename) {
|
|
|
53
53
|
}
|
|
54
54
|
export function local() {
|
|
55
55
|
return {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// ? 'pdf'
|
|
68
|
-
// : 'other';
|
|
69
|
-
// return {
|
|
70
|
-
// id: file,
|
|
71
|
-
// name: file,
|
|
72
|
-
// url: `/uploads/${file}`,
|
|
73
|
-
// type,
|
|
74
|
-
// size: stats.size,
|
|
75
|
-
// createdAt: stats.birthtime.toISOString()
|
|
76
|
-
// };
|
|
77
|
-
// })
|
|
78
|
-
// );
|
|
79
|
-
// },
|
|
56
|
+
getFile: async (id) => {
|
|
57
|
+
const filepath = resolve(fullDir, id);
|
|
58
|
+
try {
|
|
59
|
+
const data = await readFile(filepath);
|
|
60
|
+
const file = new File([data], id);
|
|
61
|
+
return file;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
80
67
|
uploadFile: async (file) => {
|
|
81
68
|
const ext = extname(file.name);
|
|
82
69
|
const id = randomUUID();
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type { ImageFieldData } from '../../types/fields.js';
|
|
3
3
|
import type { HTMLImgAttributes } from 'svelte/elements';
|
|
4
4
|
|
|
5
5
|
type Props = HTMLImgAttributes & {
|
|
6
|
-
data:
|
|
6
|
+
data: ImageFieldData;
|
|
7
|
+
pictureClass?: string;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
|
-
let {
|
|
10
|
+
let {
|
|
11
|
+
class: className = undefined,
|
|
12
|
+
pictureClass,
|
|
13
|
+
data,
|
|
14
|
+
loading = 'lazy',
|
|
15
|
+
...restProps
|
|
16
|
+
}: Props = $props();
|
|
10
17
|
</script>
|
|
11
18
|
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
<picture class={pictureClass}>
|
|
20
|
+
{#each Object.values(data.styles) as style}
|
|
21
|
+
<source srcset={style.url} type={style.mimeType} media={style.media} />
|
|
22
|
+
{/each}
|
|
23
|
+
|
|
24
|
+
<img
|
|
25
|
+
src={data.data.url}
|
|
26
|
+
alt={data.data.alt}
|
|
27
|
+
width={data.data.width}
|
|
28
|
+
height={data.data.height}
|
|
29
|
+
class={className}
|
|
30
|
+
{loading}
|
|
31
|
+
{...restProps}
|
|
32
|
+
/>
|
|
33
|
+
</picture>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ImageFieldData } from '../../types/fields.js';
|
|
2
2
|
import type { HTMLImgAttributes } from 'svelte/elements';
|
|
3
3
|
type Props = HTMLImgAttributes & {
|
|
4
|
-
data:
|
|
4
|
+
data: ImageFieldData;
|
|
5
|
+
pictureClass?: string;
|
|
5
6
|
};
|
|
6
7
|
declare const Image: import("svelte").Component<Props, {}, "">;
|
|
7
8
|
type Image = ReturnType<typeof Image>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { UploadedMediaFile } from '../media.js';
|
|
2
2
|
export interface FilesAdapter {
|
|
3
|
+
getFile: GetFile;
|
|
3
4
|
uploadFile: UploadFile;
|
|
4
5
|
}
|
|
6
|
+
export type GetFile = (id: string) => Promise<File | null>;
|
|
5
7
|
export type UploadFile = (file: File) => Promise<UploadedMediaFile>;
|
package/dist/types/adapters.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ConsentLogData } from './consent.js';
|
|
2
2
|
import type { RawEntry } from './entries.js';
|
|
3
|
+
import type { ImageFieldStyle } from './fields.js';
|
|
3
4
|
import type { FormSubmission } from './forms.js';
|
|
4
|
-
import type { MediaFile, MediaFolder } from './media.js';
|
|
5
|
+
import type { ImageStyle, MediaFile, MediaFolder, UploadedMediaFile } from './media.js';
|
|
5
6
|
import type { Session } from './sessions.js';
|
|
6
7
|
import type { User } from './users.js';
|
|
7
8
|
export type { AuthAdapter } from './adapters/auth.js';
|
|
@@ -19,6 +20,8 @@ export interface DatabaseAdapter {
|
|
|
19
20
|
createMediaFolder: CreateMediaFolder;
|
|
20
21
|
getMediaFolders: GetMediaFolders;
|
|
21
22
|
createMediaFile: CreateMediaFile;
|
|
23
|
+
getImageStyle: GetImageStyle;
|
|
24
|
+
createImageStyle: CreateImageStyle;
|
|
22
25
|
getMediaFiles: GetMediaFiles;
|
|
23
26
|
getMediaFile: GetMediaFile;
|
|
24
27
|
updateMediaFile: UpdateMediaFile;
|
|
@@ -98,6 +101,8 @@ export type UpdateMediaFile = (data: {
|
|
|
98
101
|
data: Partial<MediaFile>;
|
|
99
102
|
}) => Promise<MediaFile>;
|
|
100
103
|
export type CreateMediaFile = (data: MediaFile) => Promise<MediaFile>;
|
|
104
|
+
export type CreateImageStyle = (mediaFileId: string, file: UploadedMediaFile, style: ImageFieldStyle) => Promise<ImageStyle>;
|
|
105
|
+
export type GetImageStyle = (mediaFileId: string, style: ImageFieldStyle) => Promise<ImageStyle | null>;
|
|
101
106
|
export interface GetUserOptions {
|
|
102
107
|
data: {
|
|
103
108
|
id?: string;
|
package/dist/types/fields.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ImageStyle, MediaFile } from './media.js';
|
|
1
2
|
export type FieldType = 'text' | 'richtext' | 'number' | 'boolean' | 'date' | 'datetime' | 'file' | 'image' | 'select' | 'radio' | 'checkboxes' | 'relation' | 'object' | 'array' | 'slug' | 'seo' | 'url';
|
|
2
3
|
export interface BaseField {
|
|
3
4
|
slug: string;
|
|
@@ -62,6 +63,19 @@ export interface ImageField extends BaseField {
|
|
|
62
63
|
aspectRatio?: number;
|
|
63
64
|
thumbnail?: boolean;
|
|
64
65
|
defaultValue?: string | string[];
|
|
66
|
+
styles?: ImageFieldStyle[];
|
|
67
|
+
}
|
|
68
|
+
export interface ImageFieldStyle {
|
|
69
|
+
name: string;
|
|
70
|
+
width: number;
|
|
71
|
+
height: number;
|
|
72
|
+
format?: 'jpeg' | 'png' | 'webp' | 'avif';
|
|
73
|
+
media?: string;
|
|
74
|
+
crop?: boolean;
|
|
75
|
+
}
|
|
76
|
+
export interface ImageFieldData {
|
|
77
|
+
data: MediaFile;
|
|
78
|
+
styles: Record<string, ImageStyle>;
|
|
65
79
|
}
|
|
66
80
|
export interface SelectField extends BaseField {
|
|
67
81
|
type: 'select';
|
package/dist/types/media.d.ts
CHANGED