@lobb-js/lobb-ext-storage 0.5.8 → 0.8.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/.github/workflows/create_tag_on_version_change.yaml +45 -0
- package/.github/workflows/publish_npm_package.yaml +28 -0
- package/.vscode/settings.json +5 -0
- package/CHANGELOG.md +150 -29
- package/README.md +1 -35
- package/extensions/storage/adapters/index.ts +18 -0
- package/extensions/storage/adapters/localAdapter.ts +60 -0
- package/extensions/storage/adapters/storageAdapter.ts +13 -0
- package/extensions/storage/collections/fileSystem.ts +58 -0
- package/extensions/storage/collections/index.ts +12 -0
- package/extensions/storage/config/extensionConfigSchema.ts +15 -0
- package/extensions/storage/index.ts +26 -0
- package/extensions/storage/init.ts +6 -0
- package/extensions/storage/meta.ts +5 -0
- package/extensions/storage/migrations.ts +3 -0
- package/extensions/storage/openapi.ts +84 -0
- package/extensions/storage/studio/tests/fileManager.spec.ts +23 -0
- package/extensions/storage/studio/tests/package.json +1 -0
- package/extensions/storage/studio/tests/playwright.config.cjs +27 -0
- package/extensions/storage/studioExtension.json +1 -0
- package/extensions/storage/tests/configs/simple.ts +95 -0
- package/extensions/storage/tests/directories.test.ts +156 -0
- package/extensions/storage/tests/extraFormData.test.ts +47 -0
- package/extensions/storage/tests/files/rose.jpeg +0 -0
- package/extensions/storage/tests/files.test.ts +292 -0
- package/extensions/storage/tests/forceUpload.test.ts +72 -0
- package/extensions/storage/tests/massRemove.test.ts +189 -0
- package/extensions/storage/tests/meta.test.ts +26 -0
- package/extensions/storage/tests/recursiveDeleteMany.test.ts +208 -0
- package/extensions/storage/tests/recursiveDeleteOne.test.ts +206 -0
- package/extensions/storage/tests/storage/127 +0 -0
- package/extensions/storage/types.ts +11 -0
- package/extensions/storage/utils.ts +87 -0
- package/extensions/storage/workflows/controllers.ts +103 -0
- package/extensions/storage/workflows/index.ts +10 -0
- package/extensions/storage/workflows/services.ts +182 -0
- package/lobb.ts +54 -0
- package/package.json +32 -9
- package/scripts/postpublish.sh +12 -0
- package/scripts/prepublish.sh +17 -0
- package/studio/app.html +12 -0
- package/studio/routes/+layout.svelte +7 -0
- package/studio/routes/+layout.ts +1 -0
- package/studio/routes/[...path]/+page.svelte +6 -0
- package/svelte.config.js +23 -7
- package/todo.md +36 -0
- package/tsconfig.app.json +3 -3
- package/tsconfig.json +11 -5
- package/vite.config.ts +4 -10
- package/components.json +0 -16
- package/index.html +0 -13
- package/src/app.css +0 -124
- package/src/main.ts +0 -14
- /package/{src → extensions/storage/studio}/extension.types.ts +0 -0
- /package/{src → extensions/storage/studio}/index.ts +0 -0
- /package/{src → extensions/storage/studio}/lib/components/childrenFileExplorer.svelte +0 -0
- /package/{src → extensions/storage/studio}/lib/components/explorerNotSupported.svelte +0 -0
- /package/{src → extensions/storage/studio}/lib/components/fileExplorer.svelte +0 -0
- /package/{src → extensions/storage/studio}/lib/components/fileIcon.svelte +0 -0
- /package/{src → extensions/storage/studio}/lib/components/fileManagerBreadCrumbs.svelte +0 -0
- /package/{src → extensions/storage/studio}/lib/components/foreignKeyComponent.svelte +0 -0
- /package/{src → extensions/storage/studio}/lib/components/pages/fileExplorerPage.svelte +0 -0
- /package/{src → extensions/storage/studio}/lib/index.ts +0 -0
- /package/{src → extensions/storage/studio}/lib/utils.ts +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { Lobb } from "@lobb-js/core";
|
|
3
|
+
import { StorageAdapter } from "./adapters/index.ts";
|
|
4
|
+
import { LobbError } from "@lobb-js/core";
|
|
5
|
+
|
|
6
|
+
export async function getFileFromRequestBody(
|
|
7
|
+
c: Context,
|
|
8
|
+
): Promise<File | undefined> {
|
|
9
|
+
const contentType = c.req.header("Content-Type");
|
|
10
|
+
if (contentType?.startsWith("multipart/form-data")) {
|
|
11
|
+
const formData = await c.req.formData();
|
|
12
|
+
const entry = formData.get("file");
|
|
13
|
+
if (entry instanceof File) {
|
|
14
|
+
return entry;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// return undefined if no file or wrong content type
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function validatePathExists(path: string, lobb: Lobb) {
|
|
22
|
+
const pathArray = path.split("/").filter(Boolean);
|
|
23
|
+
if (pathArray.length > 0) {
|
|
24
|
+
const directoryName = pathArray[pathArray.length - 1];
|
|
25
|
+
pathArray.pop();
|
|
26
|
+
const directoryPath = "/" + pathArray.join("/");
|
|
27
|
+
const directory = (await lobb.collectionStore.findAll({
|
|
28
|
+
collectionName: "storage_fs",
|
|
29
|
+
params: {
|
|
30
|
+
filter: {
|
|
31
|
+
name: directoryName,
|
|
32
|
+
path: directoryPath,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
})).data[0];
|
|
36
|
+
|
|
37
|
+
if (!directory) {
|
|
38
|
+
throw new LobbError({
|
|
39
|
+
code: "BAD_REQUEST",
|
|
40
|
+
message: `The specified path does not exist. Please create it first.`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Ensures a directory path exists, creating it if necessary
|
|
48
|
+
*/
|
|
49
|
+
export async function ensurePathExists(path: string, lobb: Lobb): Promise<void> {
|
|
50
|
+
if (!path || path === "/") {
|
|
51
|
+
return; // Root always exists
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Parse the path into segments
|
|
55
|
+
const pathArray = path.split("/").filter(Boolean);
|
|
56
|
+
|
|
57
|
+
// Check and create each directory in the path
|
|
58
|
+
let currentPath = "/";
|
|
59
|
+
for (const dirName of pathArray) {
|
|
60
|
+
// Check if this directory exists
|
|
61
|
+
const existing = (await lobb.collectionStore.findAll({
|
|
62
|
+
collectionName: "storage_fs",
|
|
63
|
+
params: {
|
|
64
|
+
filter: {
|
|
65
|
+
name: dirName,
|
|
66
|
+
path: currentPath,
|
|
67
|
+
type: "directory",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
})).data;
|
|
71
|
+
|
|
72
|
+
// Create the directory if it doesn't exist
|
|
73
|
+
if (existing.length === 0) {
|
|
74
|
+
await lobb.collectionStore.createOne({
|
|
75
|
+
collectionName: "storage_fs",
|
|
76
|
+
data: {
|
|
77
|
+
name: dirName,
|
|
78
|
+
path: currentPath,
|
|
79
|
+
type: "directory",
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Update current path for next iteration
|
|
85
|
+
currentPath = currentPath + dirName + "/";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Lobb, Workflow } from "@lobb-js/core";
|
|
2
|
+
import type { Context } from "hono";
|
|
3
|
+
import type { StorageFsCreateOne, StorageFsFindOne } from "../types.ts";
|
|
4
|
+
|
|
5
|
+
export function getControllersWorkflows(): Workflow[] {
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
name: "storagePreCreateController",
|
|
9
|
+
eventName: "core.controllers.createOne.override",
|
|
10
|
+
handler: async (input, ctx) => {
|
|
11
|
+
if (input.collectionName === "storage_fs") {
|
|
12
|
+
const context = input.context as Context;
|
|
13
|
+
const lobb = context.get("lobb") as Lobb;
|
|
14
|
+
const contentType = context.req.header("Content-Type");
|
|
15
|
+
|
|
16
|
+
if (contentType?.startsWith("multipart/form-data")) {
|
|
17
|
+
const body = await context.req.parseBody({ all: true });
|
|
18
|
+
const fileEntry = body["file"];
|
|
19
|
+
|
|
20
|
+
if (!fileEntry) {
|
|
21
|
+
throw new ctx.LobbError({
|
|
22
|
+
code: "BAD_REQUEST",
|
|
23
|
+
message: "No files were provided.",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (Array.isArray(fileEntry)) {
|
|
28
|
+
throw new ctx.LobbError({
|
|
29
|
+
code: "BAD_REQUEST",
|
|
30
|
+
message: "Only one file can be uploaded at a time.",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!(fileEntry instanceof File)) {
|
|
35
|
+
throw new ctx.LobbError({
|
|
36
|
+
code: "BAD_REQUEST",
|
|
37
|
+
message: "The file shouldnt be a string",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const force = context.req.query("force") === "true";
|
|
42
|
+
|
|
43
|
+
const extraData: Record<string, string> = {};
|
|
44
|
+
for (const [key, val] of Object.entries(body)) {
|
|
45
|
+
if (key !== "file" && key !== "payload" && typeof val === "string") {
|
|
46
|
+
extraData[key] = val;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = await ctx.lobb.collectionService.createOne<StorageFsCreateOne>({
|
|
51
|
+
collectionName: "storage_fs",
|
|
52
|
+
file: fileEntry,
|
|
53
|
+
force,
|
|
54
|
+
data: extraData,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return context.json(result, 201);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "storageFindOneController",
|
|
64
|
+
eventName: "core.controllers.findOne.override",
|
|
65
|
+
handler: async (input, ctx) => {
|
|
66
|
+
if (input.collectionName === "storage_fs") {
|
|
67
|
+
const context = input.context as Context;
|
|
68
|
+
const action = context.req.query("action");
|
|
69
|
+
|
|
70
|
+
if (action === "view" || action === "download") {
|
|
71
|
+
const result = await ctx.lobb.collectionService.findOne<StorageFsFindOne>({
|
|
72
|
+
collectionName: "storage_fs",
|
|
73
|
+
id: input.id,
|
|
74
|
+
readFile: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!result.data) {
|
|
78
|
+
throw new ctx.LobbError({
|
|
79
|
+
code: "NOT_FOUND",
|
|
80
|
+
message: "The record is not found",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const headers: Record<string, string> = {
|
|
85
|
+
"Content-Type": result.data.file_mime_type,
|
|
86
|
+
};
|
|
87
|
+
if (action === "download") {
|
|
88
|
+
headers["Content-Disposition"] =
|
|
89
|
+
`attachment; filename="${result.data.name}"`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return new Response(result.file.buffer as ArrayBuffer, { status: 200, headers });
|
|
93
|
+
} else if (action) {
|
|
94
|
+
throw new ctx.LobbError({
|
|
95
|
+
code: "BAD_REQUEST",
|
|
96
|
+
message: `The passed (${action}) action query param is not supported`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ExtensionConfig } from "../config/extensionConfigSchema.ts";
|
|
2
|
+
import { getControllersWorkflows } from "./controllers.ts";
|
|
3
|
+
import { getServicesWorkflows } from "./services.ts";
|
|
4
|
+
|
|
5
|
+
export function getWorkflows(extensionConfig: ExtensionConfig) {
|
|
6
|
+
return [
|
|
7
|
+
...getServicesWorkflows(extensionConfig),
|
|
8
|
+
...getControllersWorkflows(),
|
|
9
|
+
];
|
|
10
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { Workflow } from "@lobb-js/core";
|
|
2
|
+
import type { ExtensionConfig } from "../config/extensionConfigSchema.ts";
|
|
3
|
+
import { initStorageAdapter } from "../adapters/index.ts";
|
|
4
|
+
import { validatePathExists, ensurePathExists } from "../utils.ts";
|
|
5
|
+
|
|
6
|
+
export function getServicesWorkflows(extensionConfig: ExtensionConfig): Workflow[] {
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
name: "storageFindOneService",
|
|
10
|
+
eventName: "core.service.findOne.override",
|
|
11
|
+
handler: async (input, ctx) => {
|
|
12
|
+
if (input.collectionName === "storage_fs") {
|
|
13
|
+
const result = await ctx.lobb.collectionStore.findOne({
|
|
14
|
+
collectionName: "storage_fs",
|
|
15
|
+
id: input.id,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (result.data?.type === "file" && input.readFile) {
|
|
19
|
+
const adapter = initStorageAdapter(extensionConfig);
|
|
20
|
+
const file = await adapter.readFile(result.data.id);
|
|
21
|
+
return { ...result, file: new Uint8Array(file) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "storageCreateOneServiceOverride",
|
|
30
|
+
eventName: "core.service.createOne.override",
|
|
31
|
+
handler: async (input, ctx) => {
|
|
32
|
+
if (input.collectionName === "storage_fs") {
|
|
33
|
+
if (input.file) {
|
|
34
|
+
const filePath = input.data?.path ?? "/";
|
|
35
|
+
|
|
36
|
+
if (input.force) {
|
|
37
|
+
await ensurePathExists(filePath, ctx.lobb);
|
|
38
|
+
} else if (filePath !== "/") {
|
|
39
|
+
await validatePathExists(filePath, ctx.lobb);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { path: _path, name: _name, type: _type, file_mime_type: _mime, file_size: _size, ...extraData } = input.data ?? {};
|
|
43
|
+
|
|
44
|
+
const result = await ctx.lobb.collectionStore.createOne({
|
|
45
|
+
collectionName: "storage_fs",
|
|
46
|
+
data: {
|
|
47
|
+
...extraData,
|
|
48
|
+
name: input.file.name,
|
|
49
|
+
path: filePath,
|
|
50
|
+
type: "file",
|
|
51
|
+
file_mime_type: input.file.type,
|
|
52
|
+
file_size: input.file.size,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const adapter = initStorageAdapter(extensionConfig);
|
|
57
|
+
await adapter.createFile(result.data.id, input.file);
|
|
58
|
+
|
|
59
|
+
return result;
|
|
60
|
+
} else {
|
|
61
|
+
if (input.data?.path) {
|
|
62
|
+
await validatePathExists(input.data.path, ctx.lobb);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "storageDeleteOneService",
|
|
70
|
+
eventName: "core.service.deleteOne.override",
|
|
71
|
+
handler: async (input, ctx) => {
|
|
72
|
+
if (input.collectionName === "storage_fs") {
|
|
73
|
+
const entry = (await ctx.lobb.collectionStore.findOne({
|
|
74
|
+
collectionName: "storage_fs",
|
|
75
|
+
id: input.id,
|
|
76
|
+
})).data;
|
|
77
|
+
|
|
78
|
+
const adapter = initStorageAdapter(extensionConfig);
|
|
79
|
+
if (entry.type === "file") {
|
|
80
|
+
await adapter.removeFile(entry.id);
|
|
81
|
+
} else if (entry.type === "directory") {
|
|
82
|
+
const directoryPath = entry.path + entry.name;
|
|
83
|
+
|
|
84
|
+
const entries = (await ctx.lobb.collectionService.findAll({
|
|
85
|
+
collectionName: "storage_fs",
|
|
86
|
+
params: {
|
|
87
|
+
filter: {
|
|
88
|
+
path: {
|
|
89
|
+
$starts_with: directoryPath,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
})).data;
|
|
94
|
+
|
|
95
|
+
for (let index = 0; index < entries.length; index++) {
|
|
96
|
+
const entry = entries[index];
|
|
97
|
+
if (entry.type === "file") {
|
|
98
|
+
await adapter.removeFile(entry.id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await ctx.lobb.collectionStore.deleteMany({
|
|
103
|
+
collectionName: "storage_fs",
|
|
104
|
+
filter: {
|
|
105
|
+
path: {
|
|
106
|
+
$starts_with: directoryPath,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return ctx.lobb.collectionStore.deleteOne({
|
|
113
|
+
collectionName: "storage_fs",
|
|
114
|
+
id: input.id,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "storageDeleteManyService",
|
|
121
|
+
eventName: "core.service.deleteMany.override",
|
|
122
|
+
handler: async (input, ctx) => {
|
|
123
|
+
if (input.collectionName === "storage_fs") {
|
|
124
|
+
const matchedEntries = (await ctx.lobb.collectionStore.findAll({
|
|
125
|
+
collectionName: "storage_fs",
|
|
126
|
+
params: { filter: input.filter },
|
|
127
|
+
})).data;
|
|
128
|
+
|
|
129
|
+
const adapter = initStorageAdapter(extensionConfig);
|
|
130
|
+
|
|
131
|
+
for (const entry of matchedEntries) {
|
|
132
|
+
if (entry.type === "directory") {
|
|
133
|
+
const directoryPath = entry.path + entry.name;
|
|
134
|
+
|
|
135
|
+
const nestedEntries = (await ctx.lobb.collectionStore.findAll({
|
|
136
|
+
collectionName: "storage_fs",
|
|
137
|
+
params: {
|
|
138
|
+
filter: { path: { $starts_with: directoryPath } },
|
|
139
|
+
},
|
|
140
|
+
})).data;
|
|
141
|
+
|
|
142
|
+
for (const nested of nestedEntries) {
|
|
143
|
+
if (nested.type === "file") {
|
|
144
|
+
await adapter.removeFile(nested.id);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await ctx.lobb.collectionStore.deleteMany({
|
|
149
|
+
collectionName: "storage_fs",
|
|
150
|
+
filter: { path: { $starts_with: directoryPath } },
|
|
151
|
+
});
|
|
152
|
+
} else if (entry.type === "file") {
|
|
153
|
+
await adapter.removeFile(entry.id);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return ctx.lobb.collectionStore.deleteMany({
|
|
158
|
+
collectionName: "storage_fs",
|
|
159
|
+
filter: input.filter,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "storageUpdateOneService",
|
|
166
|
+
eventName: "core.service.updateOne.override",
|
|
167
|
+
handler: async (input, ctx) => {
|
|
168
|
+
if (input.collectionName === "storage_fs") {
|
|
169
|
+
if (input.data.path) {
|
|
170
|
+
await validatePathExists(input.data.path, ctx.lobb);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return ctx.lobb.collectionStore.updateOne({
|
|
174
|
+
collectionName: "storage_fs",
|
|
175
|
+
id: input.id,
|
|
176
|
+
data: input.data,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
}
|
package/lobb.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Lobb } from "@lobb-js/core";
|
|
2
|
+
import { storage } from "./extensions/storage/index.ts";
|
|
3
|
+
|
|
4
|
+
Lobb.init({
|
|
5
|
+
project: {
|
|
6
|
+
name: "Storage Extension Test",
|
|
7
|
+
force_sync: true,
|
|
8
|
+
},
|
|
9
|
+
database: {
|
|
10
|
+
host: Bun.env.DATABASE_HOST ?? "localhost",
|
|
11
|
+
port: Number(Bun.env.DATABASE_PORT ?? 5432),
|
|
12
|
+
username: Bun.env.DATABASE_USER ?? "test",
|
|
13
|
+
password: Bun.env.DATABASE_PASSWORD ?? "test",
|
|
14
|
+
database: Bun.env.DATABASE_NAME ?? "storage_ext",
|
|
15
|
+
},
|
|
16
|
+
web_server: {
|
|
17
|
+
host: Bun.env.WEB_SERVER_HOST ?? "0.0.0.0",
|
|
18
|
+
port: Number(Bun.env.WEB_SERVER_PORT ?? 3000),
|
|
19
|
+
cors: {
|
|
20
|
+
origin: "*",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
extensions: [
|
|
24
|
+
storage({
|
|
25
|
+
adapter: "local",
|
|
26
|
+
storagePath: "./storage",
|
|
27
|
+
}),
|
|
28
|
+
],
|
|
29
|
+
collections: {
|
|
30
|
+
articles: {
|
|
31
|
+
indexes: {},
|
|
32
|
+
fields: {
|
|
33
|
+
id: {
|
|
34
|
+
type: "integer",
|
|
35
|
+
},
|
|
36
|
+
image: {
|
|
37
|
+
type: "string",
|
|
38
|
+
length: 255,
|
|
39
|
+
},
|
|
40
|
+
title: {
|
|
41
|
+
type: "string",
|
|
42
|
+
length: 255,
|
|
43
|
+
validators: {
|
|
44
|
+
required: true,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
body: {
|
|
48
|
+
type: "string",
|
|
49
|
+
length: 255,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
package/package.json
CHANGED
|
@@ -1,31 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobb-js/lobb-ext-storage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"license": "AGPL-3.0-only",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"publishConfig": {
|
|
6
7
|
"access": "public"
|
|
7
8
|
},
|
|
8
9
|
"exports": {
|
|
9
|
-
".": "./
|
|
10
|
+
".": "./extensions/storage/index.ts",
|
|
11
|
+
"./studio": {
|
|
12
|
+
"svelte": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
10
15
|
},
|
|
11
16
|
"scripts": {
|
|
12
|
-
"
|
|
13
|
-
"
|
|
17
|
+
"test": "bun run test:lobb && bun run test:studio",
|
|
18
|
+
"test:lobb": "bun test extensions/storage/tests",
|
|
19
|
+
"test:studio": "bun --bun playwright test --config extensions/storage/studio/tests/playwright.config.cjs",
|
|
20
|
+
"dev": "bun run lobb.ts",
|
|
21
|
+
"start": "LOBB_MODE=prod bun run lobb.ts",
|
|
22
|
+
"prepare": "svelte-kit sync || echo ''",
|
|
14
23
|
"preview": "vite preview",
|
|
15
|
-
"check": "svelte-check --tsconfig ./tsconfig.
|
|
24
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
25
|
+
"postinstall": "bun --bun playwright install chromium",
|
|
26
|
+
"prepublishOnly": "./scripts/prepublish.sh",
|
|
27
|
+
"postpublish": "./scripts/postpublish.sh",
|
|
28
|
+
"package": "svelte-package --input extensions/storage/studio",
|
|
29
|
+
"dev:studio": "vite dev",
|
|
30
|
+
"build:studio": "vite build"
|
|
16
31
|
},
|
|
17
32
|
"dependencies": {
|
|
18
|
-
"
|
|
19
|
-
"browser-fs-access": "^0.35.0"
|
|
33
|
+
"@lobb-js/core": "0.13.1",
|
|
34
|
+
"browser-fs-access": "^0.35.0",
|
|
35
|
+
"hono": "^4.7.0",
|
|
36
|
+
"openapi-types": "^12.1.3",
|
|
37
|
+
"path-browserify": "^1.0.1"
|
|
20
38
|
},
|
|
21
39
|
"devDependencies": {
|
|
22
|
-
"@lobb-js/studio": "0.
|
|
40
|
+
"@lobb-js/studio": "0.7.1",
|
|
23
41
|
"@lucide/svelte": "^0.563.1",
|
|
42
|
+
"@playwright/test": "^1.58.2",
|
|
43
|
+
"@sveltejs/adapter-node": "^5.5.4",
|
|
44
|
+
"@sveltejs/kit": "^2.55.0",
|
|
24
45
|
"@sveltejs/vite-plugin-svelte": "6.2.1",
|
|
46
|
+
"@sveltejs/package": "^2.5.7",
|
|
25
47
|
"@tailwindcss/vite": "^4.1.18",
|
|
26
48
|
"@tsconfig/svelte": "^5.0.6",
|
|
27
|
-
"@types/path-browserify": "^1.0.3",
|
|
28
49
|
"@types/node": "^24.10.1",
|
|
50
|
+
"@types/path-browserify": "^1.0.3",
|
|
51
|
+
"bun-types": "latest",
|
|
29
52
|
"clsx": "^2.1.1",
|
|
30
53
|
"svelte": "^5.49.1",
|
|
31
54
|
"svelte-check": "^4.3.4",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Postpublish script for @lobb-js/lobb-ext-storage
|
|
4
|
+
# Reverts package.json exports and cleans dist to avoid uncommitted changes
|
|
5
|
+
|
|
6
|
+
echo "📝 Reverting package.json exports to development mode..."
|
|
7
|
+
jq '."exports"["./studio"] = "./extensions/storage/studio/index.ts"' package.json > package.json.tmp && mv package.json.tmp package.json
|
|
8
|
+
|
|
9
|
+
echo "🧹 Cleaning dist directory..."
|
|
10
|
+
rm -rf dist
|
|
11
|
+
|
|
12
|
+
echo "✅ Postpublish complete"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Prepublish script for @lobb-js/lobb-ext-storage
|
|
4
|
+
# Builds the studio package and updates exports for publishing
|
|
5
|
+
|
|
6
|
+
echo "📦 Building studio package..."
|
|
7
|
+
bun run package
|
|
8
|
+
|
|
9
|
+
if [ $? -ne 0 ]; then
|
|
10
|
+
echo "❌ Build failed"
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
echo "📝 Updating package.json exports for publishing..."
|
|
15
|
+
jq '."exports"["./studio"] = {"svelte": "./dist/index.js", "types": "./dist/index.d.ts"}' package.json > package.json.tmp && mv package.json.tmp package.json
|
|
16
|
+
|
|
17
|
+
echo "✅ Prepublish complete"
|
package/studio/app.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<link rel="icon" href="%sveltekit.assets%/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
%sveltekit.head%
|
|
8
|
+
</head>
|
|
9
|
+
<body data-sveltekit-preload-data="hover">
|
|
10
|
+
<div style="display: contents">%sveltekit.body%</div>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ssr = false;
|
package/svelte.config.js
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import
|
|
1
|
+
import adapter from '@sveltejs/adapter-node';
|
|
2
2
|
|
|
3
|
-
/** @type {import(
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
/** @type {import('@sveltejs/kit').Config} */
|
|
4
|
+
const config = {
|
|
5
|
+
kit: {
|
|
6
|
+
adapter: adapter(),
|
|
7
|
+
paths: {
|
|
8
|
+
base: '/studio'
|
|
9
|
+
},
|
|
10
|
+
files: {
|
|
11
|
+
lib: 'studio/lib',
|
|
12
|
+
routes: 'studio/routes',
|
|
13
|
+
appTemplate: 'studio/app.html',
|
|
14
|
+
assets: 'public',
|
|
15
|
+
hooks: {
|
|
16
|
+
server: 'studio/hooks.server',
|
|
17
|
+
client: 'studio/hooks.client',
|
|
18
|
+
},
|
|
19
|
+
params: 'studio/params',
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default config;
|
package/todo.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# high priority
|
|
2
|
+
|
|
3
|
+
- move the validation logic that checks if the directory of uploaded file path exists or not in the service level and not in the controller level.
|
|
4
|
+
|
|
5
|
+
- a better way to implement this extension is to update or overright the collectionService. so that you can use the same exsting CollectionService class normally but with the ability to adjust the params of that service funciton. so the storage extension version of the normal collection overright will have different params set. for example it should have a param of type blob or File. so you shouldnt really expose services. in the mod.ts. the services already exists why expose. we shouldnt really have at all the services property
|
|
6
|
+
- remove services from this extension and from everywhere
|
|
7
|
+
- instead of creating your own service method. you can just overwrite or update the existing method
|
|
8
|
+
- for example to upload a file you can just use the collectionService mutated create version instead of creating your own function
|
|
9
|
+
- you need to also overwrite the controllers to call the new service function correctly
|
|
10
|
+
- this way is much better from exposing a bunch of service functions and more consistant
|
|
11
|
+
- you should use workflows to overwrite the controllers and the services
|
|
12
|
+
- make sure the tests work correctly
|
|
13
|
+
- remove totally the services from this extension and from the extensionSystem totally
|
|
14
|
+
- go to the `social_to_courier` and fix the storage update part.
|
|
15
|
+
|
|
16
|
+
- remove the unique index of (location, name)
|
|
17
|
+
- implement uploading a file by drag and drop
|
|
18
|
+
- implement the listview
|
|
19
|
+
|
|
20
|
+
# low priority
|
|
21
|
+
|
|
22
|
+
- you need to add a refresh button
|
|
23
|
+
- when the user tries to upload a file or a directory in a non existing.
|
|
24
|
+
directory you have to respond with a 400 Error
|
|
25
|
+
- check if a file exists by sending a GET request instead of reading the
|
|
26
|
+
directory. because thats a unit test specific to local adapter. but the first
|
|
27
|
+
way would work in all kinds of adapters. and it will make the unit tests more
|
|
28
|
+
adaptable
|
|
29
|
+
- prevent the user from not adding a / at the beggining of the path or adding a
|
|
30
|
+
/ at the end of the path when inserting or updating a storage record and make
|
|
31
|
+
sure the path is not empty
|
|
32
|
+
- implement a validation mechanism at the instatiating of the storage extension
|
|
33
|
+
that checks of all operations are successfully doable in that specific adapter
|
|
34
|
+
without issues. (really important)
|
|
35
|
+
- Allow multiple versions of a file to be stored, enabling users to access
|
|
36
|
+
previous versions if needed.
|
package/tsconfig.app.json
CHANGED
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"moduleDetection": "force",
|
|
20
20
|
"baseUrl": ".",
|
|
21
21
|
"paths": {
|
|
22
|
-
"$lib": ["./
|
|
23
|
-
"$lib/*": ["./
|
|
22
|
+
"$lib": ["./studio/lib"],
|
|
23
|
+
"$lib/*": ["./studio/lib/*"]
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
|
-
"include": ["
|
|
26
|
+
"include": ["studio/**/*.ts", "studio/**/*.js", "studio/**/*.svelte"]
|
|
27
27
|
}
|
package/tsconfig.json
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"extends": "./.svelte-kit/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"checkJs": true,
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"forceConsistentCasingInFileNames": true,
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": true
|
|
12
|
+
}
|
|
7
13
|
}
|