@lobb-js/lobb-ext-storage 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -0
- package/dist/lib/components/explorerNotSupported.svelte +1 -1
- package/dist/lib/components/fileExplorer.svelte +6 -12
- package/dist/lib/components/foreignKeyComponent.svelte +1 -1
- package/extensions/storage/collections/fileSystem.ts +14 -0
- package/extensions/storage/migrations.ts +66 -2
- package/extensions/storage/studio/index.ts +1 -0
- package/extensions/storage/studio/lib/components/explorerNotSupported.svelte +1 -1
- package/extensions/storage/studio/lib/components/fileExplorer.svelte +6 -12
- package/extensions/storage/studio/lib/components/foreignKeyComponent.svelte +1 -1
- package/extensions/storage/types.ts +1 -1
- package/extensions/storage/utils.ts +125 -31
- package/extensions/storage/workflows/services.ts +225 -100
- package/package.json +5 -5
- package/extensions/storage/tests/configs/simple.ts +0 -85
- package/extensions/storage/tests/directories.test.ts +0 -156
- package/extensions/storage/tests/extraFormData.test.ts +0 -47
- package/extensions/storage/tests/files/rose.jpeg +0 -0
- package/extensions/storage/tests/files.test.ts +0 -292
- package/extensions/storage/tests/forceUpload.test.ts +0 -72
- package/extensions/storage/tests/massRemove.test.ts +0 -189
- package/extensions/storage/tests/meta.test.ts +0 -26
- package/extensions/storage/tests/recursiveDeleteMany.test.ts +0 -208
- package/extensions/storage/tests/recursiveDeleteOne.test.ts +0 -206
- package/extensions/storage/tests/storage/127 +0 -0
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import type { Workflow } from "@lobb-js/core";
|
|
2
|
+
import { LobbError } from "@lobb-js/core";
|
|
2
3
|
import type { ExtensionConfig } from "../config/extensionConfigSchema.ts";
|
|
3
4
|
import { initStorageAdapter } from "../adapters/index.ts";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
computePathForParent,
|
|
7
|
+
ensureParentIdForPath,
|
|
8
|
+
findDescendants,
|
|
9
|
+
resolveParentIdFromPath,
|
|
10
|
+
} from "../utils.ts";
|
|
11
|
+
|
|
12
|
+
// All writes funnel through these service overrides so the invariant
|
|
13
|
+
// `path === parent.path + parent.name + "/"` is maintained on every change.
|
|
14
|
+
// Reads stay free — both `parent_id` and `path` are on the row.
|
|
15
|
+
//
|
|
16
|
+
// `parent_id` is the source of truth for the hierarchy; `path` is a cached
|
|
17
|
+
// view used for fast subtree queries and breadcrumbs. Callers may pass
|
|
18
|
+
// either (or both) to create/update — we resolve to parent_id and recompute
|
|
19
|
+
// path from the tree.
|
|
5
20
|
|
|
6
21
|
export function getServicesWorkflows(extensionConfig: ExtensionConfig): Workflow[] {
|
|
7
22
|
return [
|
|
@@ -29,153 +44,263 @@ export function getServicesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
29
44
|
name: "storageCreateOneServiceOverride",
|
|
30
45
|
eventName: "core.service.createOne.override",
|
|
31
46
|
handler: async (input, ctx) => {
|
|
32
|
-
if (input.collectionName
|
|
33
|
-
if (input.file) {
|
|
34
|
-
const filePath = input.data?.path ?? "/";
|
|
47
|
+
if (input.collectionName !== "storage_fs") return;
|
|
35
48
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
await validatePathExists(filePath, ctx.lobb);
|
|
40
|
-
}
|
|
49
|
+
const raw = input.data ?? {};
|
|
50
|
+
const explicitParentId = raw.parent_id ?? null;
|
|
51
|
+
const pathInput: string = raw.path ?? "/";
|
|
41
52
|
|
|
42
|
-
|
|
53
|
+
// Resolve parent_id: explicit wins, else derive from the path.
|
|
54
|
+
// `force` (uploads) creates missing directories along the way.
|
|
55
|
+
const parentId = explicitParentId != null
|
|
56
|
+
? Number(explicitParentId)
|
|
57
|
+
: input.force
|
|
58
|
+
? await ensureParentIdForPath(pathInput, ctx.lobb)
|
|
59
|
+
: await resolveParentIdFromPath(pathInput, ctx.lobb);
|
|
43
60
|
|
|
44
|
-
|
|
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
|
-
});
|
|
61
|
+
const cachedPath = await computePathForParent(parentId, ctx.lobb);
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
const {
|
|
64
|
+
path: _path,
|
|
65
|
+
parent_id: _pid,
|
|
66
|
+
name: _name,
|
|
67
|
+
type: _type,
|
|
68
|
+
file_mime_type: _mime,
|
|
69
|
+
file_size: _size,
|
|
70
|
+
...extraData
|
|
71
|
+
} = raw;
|
|
58
72
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
if (input.file) {
|
|
74
|
+
const result = await ctx.lobb.collectionStore.createOne({
|
|
75
|
+
collectionName: "storage_fs",
|
|
76
|
+
data: {
|
|
77
|
+
...extraData,
|
|
78
|
+
name: input.file.name,
|
|
79
|
+
parent_id: parentId,
|
|
80
|
+
path: cachedPath,
|
|
81
|
+
type: "file",
|
|
82
|
+
file_mime_type: input.file.type,
|
|
83
|
+
file_size: input.file.size,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const adapter = initStorageAdapter(extensionConfig);
|
|
88
|
+
await adapter.createFile(result.data.id, input.file);
|
|
89
|
+
return result;
|
|
65
90
|
}
|
|
91
|
+
|
|
92
|
+
// Non-file writes (directories or pure metadata records) — let the
|
|
93
|
+
// generic store handle it after we stamp parent_id + path.
|
|
94
|
+
return await ctx.lobb.collectionStore.createOne({
|
|
95
|
+
collectionName: "storage_fs",
|
|
96
|
+
data: {
|
|
97
|
+
...extraData,
|
|
98
|
+
name: raw.name,
|
|
99
|
+
parent_id: parentId,
|
|
100
|
+
path: cachedPath,
|
|
101
|
+
type: raw.type ?? "directory",
|
|
102
|
+
...(raw.icon !== undefined ? { icon: raw.icon } : {}),
|
|
103
|
+
...(raw.is_pinned_sidebar !== undefined ? { is_pinned_sidebar: raw.is_pinned_sidebar } : {}),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
66
106
|
},
|
|
67
107
|
},
|
|
68
108
|
{
|
|
69
109
|
name: "storageDeleteOneService",
|
|
70
110
|
eventName: "core.service.deleteOne.override",
|
|
71
111
|
handler: async (input, ctx) => {
|
|
72
|
-
if (input.collectionName
|
|
73
|
-
|
|
112
|
+
if (input.collectionName !== "storage_fs") return;
|
|
113
|
+
|
|
114
|
+
const entry = (await ctx.lobb.collectionStore.findOne({
|
|
115
|
+
collectionName: "storage_fs",
|
|
116
|
+
id: input.id,
|
|
117
|
+
})).data;
|
|
118
|
+
if (!entry) {
|
|
119
|
+
return ctx.lobb.collectionStore.deleteOne({
|
|
74
120
|
collectionName: "storage_fs",
|
|
75
121
|
id: input.id,
|
|
76
|
-
})
|
|
122
|
+
});
|
|
123
|
+
}
|
|
77
124
|
|
|
78
|
-
|
|
79
|
-
if (entry.type === "file") {
|
|
80
|
-
await adapter.removeFile(entry.id);
|
|
81
|
-
} else if (entry.type === "directory") {
|
|
82
|
-
const directoryPath = entry.path + entry.name;
|
|
125
|
+
const adapter = initStorageAdapter(extensionConfig);
|
|
83
126
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
}
|
|
127
|
+
if (entry.type === "file") {
|
|
128
|
+
await adapter.removeFile(entry.id);
|
|
129
|
+
} else if (entry.type === "directory") {
|
|
130
|
+
// Cascade leaf-first so each deleted row has no remaining children
|
|
131
|
+
// referencing it — Lobb's core FK-children check would otherwise
|
|
132
|
+
// block the delete. findDescendants returns BFS (shallow → deep);
|
|
133
|
+
// reverse to walk deep → shallow.
|
|
134
|
+
const descendants = await findDescendants(entry.id, ctx.lobb);
|
|
135
|
+
for (const d of [...descendants].reverse()) {
|
|
136
|
+
if (d.type === "file") {
|
|
137
|
+
await adapter.removeFile(d.id);
|
|
100
138
|
}
|
|
101
|
-
|
|
102
|
-
await ctx.lobb.collectionStore.deleteMany({
|
|
139
|
+
await ctx.lobb.collectionStore.deleteOne({
|
|
103
140
|
collectionName: "storage_fs",
|
|
104
|
-
|
|
105
|
-
path: {
|
|
106
|
-
$starts_with: directoryPath,
|
|
107
|
-
},
|
|
108
|
-
},
|
|
141
|
+
id: String(d.id),
|
|
109
142
|
});
|
|
110
143
|
}
|
|
111
|
-
|
|
112
|
-
return ctx.lobb.collectionStore.deleteOne({
|
|
113
|
-
collectionName: "storage_fs",
|
|
114
|
-
id: input.id,
|
|
115
|
-
});
|
|
116
144
|
}
|
|
145
|
+
|
|
146
|
+
return ctx.lobb.collectionStore.deleteOne({
|
|
147
|
+
collectionName: "storage_fs",
|
|
148
|
+
id: input.id,
|
|
149
|
+
});
|
|
117
150
|
},
|
|
118
151
|
},
|
|
119
152
|
{
|
|
120
153
|
name: "storageDeleteManyService",
|
|
121
154
|
eventName: "core.service.deleteMany.override",
|
|
122
155
|
handler: async (input, ctx) => {
|
|
123
|
-
if (input.collectionName
|
|
124
|
-
const matchedEntries = (await ctx.lobb.collectionStore.findAll({
|
|
125
|
-
collectionName: "storage_fs",
|
|
126
|
-
params: { filter: input.filter },
|
|
127
|
-
})).data;
|
|
156
|
+
if (input.collectionName !== "storage_fs") return;
|
|
128
157
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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;
|
|
158
|
+
const matched = (await ctx.lobb.collectionStore.findAll({
|
|
159
|
+
collectionName: "storage_fs",
|
|
160
|
+
params: { filter: input.filter },
|
|
161
|
+
})).data;
|
|
141
162
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
163
|
+
const adapter = initStorageAdapter(extensionConfig);
|
|
164
|
+
// Collect every row to remove, BFS shallow→deep. We delete in
|
|
165
|
+
// reverse order so leaves are dropped before their parents — that
|
|
166
|
+
// keeps Lobb's children FK check happy.
|
|
167
|
+
const ordered: any[] = [];
|
|
168
|
+
const seen = new Set<number>();
|
|
169
|
+
for (const entry of matched) {
|
|
170
|
+
if (seen.has(entry.id)) continue;
|
|
171
|
+
seen.add(entry.id);
|
|
172
|
+
ordered.push(entry);
|
|
173
|
+
if (entry.type === "directory") {
|
|
174
|
+
for (const d of await findDescendants(entry.id, ctx.lobb)) {
|
|
175
|
+
if (seen.has(d.id)) continue;
|
|
176
|
+
seen.add(d.id);
|
|
177
|
+
ordered.push(d);
|
|
154
178
|
}
|
|
155
179
|
}
|
|
180
|
+
}
|
|
156
181
|
|
|
157
|
-
|
|
182
|
+
for (const row of [...ordered].reverse()) {
|
|
183
|
+
if (row.type === "file") await adapter.removeFile(row.id);
|
|
184
|
+
await ctx.lobb.collectionStore.deleteOne({
|
|
158
185
|
collectionName: "storage_fs",
|
|
159
|
-
|
|
186
|
+
id: String(row.id),
|
|
160
187
|
});
|
|
161
188
|
}
|
|
189
|
+
|
|
190
|
+
// affectedCount reflects what the caller asked to delete (the
|
|
191
|
+
// matched rows). Cascaded descendants are an implementation detail.
|
|
192
|
+
return { affectedCount: matched.length };
|
|
162
193
|
},
|
|
163
194
|
},
|
|
164
195
|
{
|
|
165
196
|
name: "storageUpdateOneService",
|
|
166
197
|
eventName: "core.service.updateOne.override",
|
|
167
198
|
handler: async (input, ctx) => {
|
|
168
|
-
if (input.collectionName
|
|
169
|
-
if (input.data.path) {
|
|
170
|
-
await validatePathExists(input.data.path, ctx.lobb);
|
|
171
|
-
}
|
|
199
|
+
if (input.collectionName !== "storage_fs") return;
|
|
172
200
|
|
|
201
|
+
const current = (await ctx.lobb.collectionStore.findOne({
|
|
202
|
+
collectionName: "storage_fs",
|
|
203
|
+
id: input.id,
|
|
204
|
+
})).data;
|
|
205
|
+
if (!current) {
|
|
173
206
|
return ctx.lobb.collectionStore.updateOne({
|
|
174
207
|
collectionName: "storage_fs",
|
|
175
208
|
id: input.id,
|
|
176
209
|
data: input.data,
|
|
177
210
|
});
|
|
178
211
|
}
|
|
212
|
+
|
|
213
|
+
const data: any = { ...input.data };
|
|
214
|
+
|
|
215
|
+
// Translate any path-based reparenting into a parent_id update so
|
|
216
|
+
// the rest of this handler has a single canonical signal to work
|
|
217
|
+
// with. Explicit parent_id wins if both are present.
|
|
218
|
+
if (data.path != null && data.parent_id == null) {
|
|
219
|
+
data.parent_id = await resolveParentIdFromPath(data.path, ctx.lobb);
|
|
220
|
+
}
|
|
221
|
+
// The path field is derived — never let a caller overwrite it raw.
|
|
222
|
+
delete data.path;
|
|
223
|
+
|
|
224
|
+
// If the row's location in the tree is changing (name or parent),
|
|
225
|
+
// recompute its own path and cascade the change to every descendant.
|
|
226
|
+
const nameChanged = data.name != null && data.name !== current.name;
|
|
227
|
+
const parentChanged =
|
|
228
|
+
Object.prototype.hasOwnProperty.call(data, "parent_id") &&
|
|
229
|
+
(data.parent_id ?? null) !== (current.parent_id ?? null);
|
|
230
|
+
|
|
231
|
+
if (nameChanged || parentChanged) {
|
|
232
|
+
const newParentId = parentChanged ? (data.parent_id ?? null) : current.parent_id;
|
|
233
|
+
const newName = nameChanged ? data.name : current.name;
|
|
234
|
+
|
|
235
|
+
// Cycle prevention: the new parent must not be the row itself,
|
|
236
|
+
// and walking up from it must never hit the row's own id.
|
|
237
|
+
if (parentChanged && newParentId != null) {
|
|
238
|
+
if (Number(newParentId) === Number(input.id)) {
|
|
239
|
+
throw new LobbError({
|
|
240
|
+
code: "BAD_REQUEST",
|
|
241
|
+
message: "A directory cannot be its own parent.",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
let cur: number | null = Number(newParentId);
|
|
245
|
+
while (cur != null) {
|
|
246
|
+
if (cur === Number(input.id)) {
|
|
247
|
+
throw new LobbError({
|
|
248
|
+
code: "BAD_REQUEST",
|
|
249
|
+
message: "Cannot move a directory into one of its own descendants.",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const row: any = (await ctx.lobb.collectionStore.findOne({
|
|
253
|
+
collectionName: "storage_fs",
|
|
254
|
+
id: String(cur),
|
|
255
|
+
})).data;
|
|
256
|
+
if (!row) break;
|
|
257
|
+
cur = row.parent_id ?? null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const newSelfPath = await computePathForParent(newParentId, ctx.lobb);
|
|
262
|
+
|
|
263
|
+
const result = await ctx.lobb.collectionStore.updateOne({
|
|
264
|
+
collectionName: "storage_fs",
|
|
265
|
+
id: input.id,
|
|
266
|
+
data: { ...data, path: newSelfPath },
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Cascade: for a directory, every descendant's `path` is anchored
|
|
270
|
+
// to this row's name + its own path. Recompute by walking the
|
|
271
|
+
// tree once in memory — no DB round trip per descendant.
|
|
272
|
+
if (current.type === "directory") {
|
|
273
|
+
const descendants = await findDescendants(Number(input.id), ctx.lobb);
|
|
274
|
+
const byId = new Map<number, any>();
|
|
275
|
+
for (const d of descendants) byId.set(d.id, d);
|
|
276
|
+
// The directory's OWN full path (= its parent's path + its name)
|
|
277
|
+
// is what every direct child stores in `path`.
|
|
278
|
+
const newDirFullPath = newSelfPath === "/" ? "/" + newName : newSelfPath + "/" + newName;
|
|
279
|
+
const pathOf = (id: number | null): string => {
|
|
280
|
+
if (id == null) return "/";
|
|
281
|
+
if (id === Number(input.id)) return newDirFullPath;
|
|
282
|
+
const r = byId.get(id);
|
|
283
|
+
if (!r) return "/";
|
|
284
|
+
const parentFull = pathOf(r.parent_id ?? null);
|
|
285
|
+
return parentFull === "/" ? "/" + r.name : parentFull + "/" + r.name;
|
|
286
|
+
};
|
|
287
|
+
for (const d of descendants) {
|
|
288
|
+
await ctx.lobb.collectionStore.updateOne({
|
|
289
|
+
collectionName: "storage_fs",
|
|
290
|
+
id: String(d.id),
|
|
291
|
+
data: { path: pathOf(d.parent_id ?? null) },
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return ctx.lobb.collectionStore.updateOne({
|
|
300
|
+
collectionName: "storage_fs",
|
|
301
|
+
id: input.id,
|
|
302
|
+
data,
|
|
303
|
+
});
|
|
179
304
|
},
|
|
180
305
|
},
|
|
181
306
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobb-js/lobb-ext-storage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"test": "bun test
|
|
22
|
-
"test:lobb": "bun test
|
|
21
|
+
"test": "bun test tests",
|
|
22
|
+
"test:lobb": "bun test tests",
|
|
23
23
|
"dev": "bun run --watch lobb.ts",
|
|
24
24
|
"dev:studio": "vite dev",
|
|
25
25
|
"build": "vite build",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"package": "svelte-package --input extensions/storage/studio"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@lobb-js/core": "^0.
|
|
35
|
+
"@lobb-js/core": "^0.38.1",
|
|
36
36
|
"browser-fs-access": "^0.35.0",
|
|
37
37
|
"hono": "^4.7.0",
|
|
38
38
|
"openapi-types": "^12.1.3",
|
|
39
39
|
"path-browserify": "^1.0.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@lobb-js/studio": "^0.
|
|
42
|
+
"@lobb-js/studio": "^0.44.1",
|
|
43
43
|
"@lucide/svelte": "^0.563.1",
|
|
44
44
|
"@sveltejs/adapter-node": "^5.5.4",
|
|
45
45
|
"@sveltejs/kit": "^2.60.1",
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import type { Config } from "@lobb-js/core";
|
|
2
|
-
import storage from "../../index.ts";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
|
|
6
|
-
export function createSimpleConfig(uploadsPath?: string): Config & { uploadsPath: string } {
|
|
7
|
-
const resolvedUploadsPath = uploadsPath ?? join(tmpdir(), "lobb_storage_" + crypto.randomUUID());
|
|
8
|
-
return {
|
|
9
|
-
uploadsPath: resolvedUploadsPath,
|
|
10
|
-
project: {
|
|
11
|
-
name: "Lobb",
|
|
12
|
-
force_sync: true,
|
|
13
|
-
},
|
|
14
|
-
database: {
|
|
15
|
-
host: "localhost",
|
|
16
|
-
port: 5432,
|
|
17
|
-
username: "test",
|
|
18
|
-
password: "test",
|
|
19
|
-
database: "*",
|
|
20
|
-
},
|
|
21
|
-
web_server: {
|
|
22
|
-
host: "0.0.0.0",
|
|
23
|
-
port: 0,
|
|
24
|
-
cors: {
|
|
25
|
-
origin: "*",
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
extensions: [
|
|
29
|
-
storage({
|
|
30
|
-
adapter: "local",
|
|
31
|
-
uploadsPath: resolvedUploadsPath,
|
|
32
|
-
}),
|
|
33
|
-
],
|
|
34
|
-
collections: {
|
|
35
|
-
articles: {
|
|
36
|
-
indexes: {},
|
|
37
|
-
fields: {
|
|
38
|
-
id: {
|
|
39
|
-
type: "integer",
|
|
40
|
-
},
|
|
41
|
-
image: {
|
|
42
|
-
type: "string",
|
|
43
|
-
length: 255,
|
|
44
|
-
},
|
|
45
|
-
title: {
|
|
46
|
-
type: "string",
|
|
47
|
-
length: 255,
|
|
48
|
-
required: true,
|
|
49
|
-
},
|
|
50
|
-
description: {
|
|
51
|
-
type: "string",
|
|
52
|
-
length: 255,
|
|
53
|
-
},
|
|
54
|
-
body: {
|
|
55
|
-
type: "string",
|
|
56
|
-
length: 255,
|
|
57
|
-
required: true,
|
|
58
|
-
},
|
|
59
|
-
status: {
|
|
60
|
-
type: "string",
|
|
61
|
-
length: 255,
|
|
62
|
-
enum: ["public", "private"],
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
comments: {
|
|
67
|
-
indexes: {},
|
|
68
|
-
fields: {
|
|
69
|
-
id: {
|
|
70
|
-
type: "integer",
|
|
71
|
-
},
|
|
72
|
-
body: {
|
|
73
|
-
type: "string",
|
|
74
|
-
length: 255,
|
|
75
|
-
required: true,
|
|
76
|
-
},
|
|
77
|
-
article_id: {
|
|
78
|
-
type: "integer",
|
|
79
|
-
required: true,
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { Lobb } from "@lobb-js/core";
|
|
2
|
-
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|
3
|
-
import { createSimpleConfig } from "./configs/simple.ts";
|
|
4
|
-
|
|
5
|
-
describe("Directories Operations", () => {
|
|
6
|
-
let lobb: Lobb;
|
|
7
|
-
let baseUrl: string;
|
|
8
|
-
let createdRecordId: number;
|
|
9
|
-
|
|
10
|
-
beforeAll(async () => {
|
|
11
|
-
const config = createSimpleConfig();
|
|
12
|
-
lobb = await Lobb.init(config);
|
|
13
|
-
baseUrl = `http://127.0.0.1:${lobb.webServer.port}`;
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
afterAll(async () => {
|
|
17
|
-
await lobb.close();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("should create a directory successfully", async () => {
|
|
21
|
-
const response = await fetch(
|
|
22
|
-
`${baseUrl}/api/collections/storage_fs`,
|
|
23
|
-
{
|
|
24
|
-
method: "POST",
|
|
25
|
-
body: JSON.stringify({
|
|
26
|
-
data: {
|
|
27
|
-
name: "test_directory",
|
|
28
|
-
path: "/",
|
|
29
|
-
},
|
|
30
|
-
}),
|
|
31
|
-
},
|
|
32
|
-
);
|
|
33
|
-
const data = await response.json();
|
|
34
|
-
|
|
35
|
-
expect(data).toMatchObject({
|
|
36
|
-
data: {
|
|
37
|
-
name: "test_directory",
|
|
38
|
-
path: "/",
|
|
39
|
-
type: "directory",
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
createdRecordId = data.data.id;
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("should prevent creating a directory in a non existing path", async () => {
|
|
47
|
-
const response = await fetch(
|
|
48
|
-
`${baseUrl}/api/collections/storage_fs`,
|
|
49
|
-
{
|
|
50
|
-
method: "POST",
|
|
51
|
-
body: JSON.stringify({
|
|
52
|
-
data: {
|
|
53
|
-
name: "another_dir",
|
|
54
|
-
path: "/none_existing_directory",
|
|
55
|
-
},
|
|
56
|
-
}),
|
|
57
|
-
},
|
|
58
|
-
);
|
|
59
|
-
const data = await response.json();
|
|
60
|
-
|
|
61
|
-
expect(data).toEqual({
|
|
62
|
-
code: "BAD_REQUEST",
|
|
63
|
-
status: 400,
|
|
64
|
-
message: "The specified path does not exist. Please create it first.",
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("should respond with the record of the requested directory entry", async () => {
|
|
69
|
-
const response = await fetch(
|
|
70
|
-
`${baseUrl}/api/collections/storage_fs/${createdRecordId}`,
|
|
71
|
-
{
|
|
72
|
-
method: "GET",
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
const data = await response.json();
|
|
76
|
-
|
|
77
|
-
expect(data).toMatchObject({
|
|
78
|
-
data: {
|
|
79
|
-
name: "test_directory",
|
|
80
|
-
path: "/",
|
|
81
|
-
type: "directory",
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("should list all file system entries", async () => {
|
|
87
|
-
const response = await fetch(
|
|
88
|
-
`${baseUrl}/api/collections/storage_fs`,
|
|
89
|
-
{
|
|
90
|
-
method: "GET",
|
|
91
|
-
},
|
|
92
|
-
);
|
|
93
|
-
const data = await response.json();
|
|
94
|
-
|
|
95
|
-
expect(data).toMatchObject({
|
|
96
|
-
data: [
|
|
97
|
-
{
|
|
98
|
-
name: "test_directory",
|
|
99
|
-
path: "/",
|
|
100
|
-
type: "directory",
|
|
101
|
-
},
|
|
102
|
-
],
|
|
103
|
-
meta: {
|
|
104
|
-
totalCount: 1,
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("should list all file system entries using a filter", async () => {
|
|
110
|
-
const response = await fetch(
|
|
111
|
-
`${baseUrl}/api/collections/storage_fs/search`,
|
|
112
|
-
{
|
|
113
|
-
method: "POST",
|
|
114
|
-
body: JSON.stringify({
|
|
115
|
-
filter: {
|
|
116
|
-
path: {
|
|
117
|
-
$starts_with: "/",
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
}),
|
|
121
|
-
},
|
|
122
|
-
);
|
|
123
|
-
const data = await response.json();
|
|
124
|
-
|
|
125
|
-
expect(data).toMatchObject({
|
|
126
|
-
data: [
|
|
127
|
-
{
|
|
128
|
-
name: "test_directory",
|
|
129
|
-
path: "/",
|
|
130
|
-
type: "directory",
|
|
131
|
-
},
|
|
132
|
-
],
|
|
133
|
-
meta: {
|
|
134
|
-
totalCount: 1,
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("should delete the directory successully", async () => {
|
|
140
|
-
const response = await fetch(
|
|
141
|
-
`${baseUrl}/api/collections/storage_fs/${createdRecordId}`,
|
|
142
|
-
{
|
|
143
|
-
method: "DELETE",
|
|
144
|
-
},
|
|
145
|
-
);
|
|
146
|
-
const data = await response.json();
|
|
147
|
-
|
|
148
|
-
expect(data).toMatchObject({
|
|
149
|
-
data: {
|
|
150
|
-
name: "test_directory",
|
|
151
|
-
path: "/",
|
|
152
|
-
type: "directory",
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
});
|