@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
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { FolderX } from "lucide-svelte";
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
|
-
<div class="border rounded-md p-4 bg-muted
|
|
5
|
+
<div class="border rounded-md p-4 bg-muted flex items-center flex-col text-muted-foreground gap-2">
|
|
6
6
|
<FolderX size={35} />
|
|
7
7
|
<div class="flex flex-col justify-center items-center">
|
|
8
8
|
<div class="text-sm font-bold">Storage file system is not supported here</div>
|
|
@@ -67,10 +67,7 @@
|
|
|
67
67
|
const newFolderName = prompt("Please enter a name for the new folder:");
|
|
68
68
|
if (newFolderName) {
|
|
69
69
|
const response = await utils.lobb.createOne("storage_fs", {
|
|
70
|
-
name: newFolderName,
|
|
71
|
-
path: location,
|
|
72
|
-
type: "directory",
|
|
73
|
-
...foreignKeyObject,
|
|
70
|
+
data: { name: newFolderName, path: location, type: "directory", ...foreignKeyObject },
|
|
74
71
|
});
|
|
75
72
|
const result = await response.json();
|
|
76
73
|
if (result.error) {
|
|
@@ -94,10 +91,7 @@
|
|
|
94
91
|
if (file) {
|
|
95
92
|
const response = await utils.lobb.createOne(
|
|
96
93
|
"storage_fs",
|
|
97
|
-
{
|
|
98
|
-
path: location,
|
|
99
|
-
...foreignKeyObject,
|
|
100
|
-
},
|
|
94
|
+
{ data: { path: location, ...foreignKeyObject } },
|
|
101
95
|
file,
|
|
102
96
|
);
|
|
103
97
|
const result = await response.json();
|
|
@@ -195,7 +189,7 @@
|
|
|
195
189
|
<div class="flex h-full w-full flex-col bg-background {className}">
|
|
196
190
|
<!-- file explorer header -->
|
|
197
191
|
<div
|
|
198
|
-
class="flex h-
|
|
192
|
+
class="flex h-12 items-center justify-between border-b px-2"
|
|
199
193
|
>
|
|
200
194
|
<div class="flex items-center justify-between gap-2">
|
|
201
195
|
{@render topLeftHeader?.()}
|
|
@@ -205,14 +199,14 @@
|
|
|
205
199
|
<!-- entries view toggle -->
|
|
206
200
|
<Button
|
|
207
201
|
variant="outline"
|
|
208
|
-
|
|
202
|
+
size="sm"
|
|
209
203
|
Icon={icons.FolderPlus}
|
|
210
204
|
onclick={handleCreateFolder}
|
|
211
205
|
>
|
|
212
206
|
New folder
|
|
213
207
|
</Button>
|
|
214
208
|
<Button
|
|
215
|
-
|
|
209
|
+
size="sm"
|
|
216
210
|
Icon={icons.FilePlus}
|
|
217
211
|
onclick={() => { handleFileUpload() }}
|
|
218
212
|
>
|
|
@@ -221,7 +215,7 @@
|
|
|
221
215
|
</div>
|
|
222
216
|
</div>
|
|
223
217
|
<!-- file explorer body -->
|
|
224
|
-
<div class="flex flex-1 overflow-auto h-[calc(100vh-
|
|
218
|
+
<div class="flex flex-1 overflow-auto h-[calc(100vh-8rem)]">
|
|
225
219
|
<ContextMenu.Root bind:open={contextMenuOpen}>
|
|
226
220
|
<ContextMenu.Trigger
|
|
227
221
|
class="z-10 flex flex-1 flex-wrap content-start items-start justify-start gap-4 p-4"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
</script>
|
|
30
30
|
|
|
31
|
-
<button onclick={handleClick} class="flex flex-col items-center gap-2 w-full border p-4 rounded-md bg-muted
|
|
31
|
+
<button onclick={handleClick} class="flex flex-col items-center gap-2 w-full border p-4 rounded-md bg-muted">
|
|
32
32
|
{#if value}
|
|
33
33
|
<FileIcon entry={value} {utils} />
|
|
34
34
|
{:else}
|
|
@@ -16,6 +16,20 @@ export function getFileSystemCollection(config: ExtensionConfig): CollectionConf
|
|
|
16
16
|
"length": 255,
|
|
17
17
|
"required": true,
|
|
18
18
|
},
|
|
19
|
+
// Source of truth for the hierarchy. null = root entry.
|
|
20
|
+
// The directory itself is referenced; deleting a directory cascades
|
|
21
|
+
// through the storage service (see services.ts) so the FK doesn't need
|
|
22
|
+
// to model ON DELETE — the service handles it.
|
|
23
|
+
"parent_id": {
|
|
24
|
+
"type": "integer",
|
|
25
|
+
"references": {
|
|
26
|
+
"collection": "storage_fs",
|
|
27
|
+
"field": "id",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
// Derived cache, NOT the source of truth. Maintained on every write by
|
|
31
|
+
// the storage service so subtree queries and breadcrumbs stay O(1).
|
|
32
|
+
// Invariant: path === parent.path + parent.name + "/" (or "/" at root).
|
|
19
33
|
"path": {
|
|
20
34
|
"type": "string",
|
|
21
35
|
"length": 255,
|
|
@@ -1,3 +1,67 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Migrations } from "@lobb-js/core";
|
|
2
|
+
import { Lobb } from "@lobb-js/core";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Backfill `parent_id` for any row in storage_fs that still has it null.
|
|
6
|
+
* Resolves the parent by walking the existing `path` string and matching
|
|
7
|
+
* the (parent_path, name) of every directory record. Idempotent — running
|
|
8
|
+
* it twice is safe; rows that already have parent_id are skipped.
|
|
9
|
+
*/
|
|
10
|
+
export async function backfillStorageFsParentId(lobb: Lobb): Promise<void> {
|
|
11
|
+
const rows = (await lobb.collectionService.findAll({
|
|
12
|
+
collectionName: "storage_fs",
|
|
13
|
+
params: { limit: 1000000 },
|
|
14
|
+
})).data;
|
|
15
|
+
if (rows.length === 0) return;
|
|
16
|
+
|
|
17
|
+
// Index every directory by (path, name) so we can resolve any child's
|
|
18
|
+
// parent in one lookup. This is the same key that resolveParentIdFromPath
|
|
19
|
+
// walks at runtime, just precomputed.
|
|
20
|
+
const dirByKey = new Map<string, number>();
|
|
21
|
+
for (const r of rows as any[]) {
|
|
22
|
+
if (r.type === "directory") {
|
|
23
|
+
dirByKey.set(`${r.path}::${r.name}`, r.id);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parentIdFor(row: any): number | null {
|
|
28
|
+
const path: string = row.path ?? "/";
|
|
29
|
+
if (path === "/" || path === "") return null;
|
|
30
|
+
const segments = path.split("/").filter(Boolean);
|
|
31
|
+
if (segments.length === 0) return null;
|
|
32
|
+
const lastName = segments[segments.length - 1];
|
|
33
|
+
// No-trailing-slash convention: root is "/", deeper levels are
|
|
34
|
+
// "/foo" or "/foo/bar". Older data may have either form; this
|
|
35
|
+
// resolves only the normalized one. Loose-format rows get retried
|
|
36
|
+
// on the next pass.
|
|
37
|
+
const parentDirPath =
|
|
38
|
+
segments.length > 1 ? "/" + segments.slice(0, -1).join("/") : "/";
|
|
39
|
+
const key = `${parentDirPath}::${lastName}`;
|
|
40
|
+
return dirByKey.get(key) ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const row of rows as any[]) {
|
|
44
|
+
if (row.parent_id != null) continue;
|
|
45
|
+
const pid = parentIdFor(row);
|
|
46
|
+
if (pid != null) {
|
|
47
|
+
await lobb.collectionStore.updateOne({
|
|
48
|
+
collectionName: "storage_fs",
|
|
49
|
+
id: row.id,
|
|
50
|
+
data: { parent_id: pid },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const migrations: Migrations = {
|
|
57
|
+
// The storage_fs schema gained `parent_id` (FK → storage_fs.id). It is now
|
|
58
|
+
// the source of truth for the hierarchy; `path` is a derived cache. For
|
|
59
|
+
// existing rows, walk the old path strings and stamp each row's parent_id
|
|
60
|
+
// with the directory that already represents its parent path. Rows whose
|
|
61
|
+
// path is "/" stay at parent_id = null (root entries).
|
|
62
|
+
"backfill_storage_fs_parent_id": {
|
|
63
|
+
up: async () => {
|
|
64
|
+
await backfillStorageFsParentId(Lobb.instance);
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { FolderX } from "lucide-svelte";
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
|
-
<div class="border rounded-md p-4 bg-muted
|
|
5
|
+
<div class="border rounded-md p-4 bg-muted flex items-center flex-col text-muted-foreground gap-2">
|
|
6
6
|
<FolderX size={35} />
|
|
7
7
|
<div class="flex flex-col justify-center items-center">
|
|
8
8
|
<div class="text-sm font-bold">Storage file system is not supported here</div>
|
|
@@ -67,10 +67,7 @@
|
|
|
67
67
|
const newFolderName = prompt("Please enter a name for the new folder:");
|
|
68
68
|
if (newFolderName) {
|
|
69
69
|
const response = await utils.lobb.createOne("storage_fs", {
|
|
70
|
-
name: newFolderName,
|
|
71
|
-
path: location,
|
|
72
|
-
type: "directory",
|
|
73
|
-
...foreignKeyObject,
|
|
70
|
+
data: { name: newFolderName, path: location, type: "directory", ...foreignKeyObject },
|
|
74
71
|
});
|
|
75
72
|
const result = await response.json();
|
|
76
73
|
if (result.error) {
|
|
@@ -94,10 +91,7 @@
|
|
|
94
91
|
if (file) {
|
|
95
92
|
const response = await utils.lobb.createOne(
|
|
96
93
|
"storage_fs",
|
|
97
|
-
{
|
|
98
|
-
path: location,
|
|
99
|
-
...foreignKeyObject,
|
|
100
|
-
},
|
|
94
|
+
{ data: { path: location, ...foreignKeyObject } },
|
|
101
95
|
file,
|
|
102
96
|
);
|
|
103
97
|
const result = await response.json();
|
|
@@ -195,7 +189,7 @@
|
|
|
195
189
|
<div class="flex h-full w-full flex-col bg-background {className}">
|
|
196
190
|
<!-- file explorer header -->
|
|
197
191
|
<div
|
|
198
|
-
class="flex h-
|
|
192
|
+
class="flex h-12 items-center justify-between border-b px-2"
|
|
199
193
|
>
|
|
200
194
|
<div class="flex items-center justify-between gap-2">
|
|
201
195
|
{@render topLeftHeader?.()}
|
|
@@ -205,14 +199,14 @@
|
|
|
205
199
|
<!-- entries view toggle -->
|
|
206
200
|
<Button
|
|
207
201
|
variant="outline"
|
|
208
|
-
|
|
202
|
+
size="sm"
|
|
209
203
|
Icon={icons.FolderPlus}
|
|
210
204
|
onclick={handleCreateFolder}
|
|
211
205
|
>
|
|
212
206
|
New folder
|
|
213
207
|
</Button>
|
|
214
208
|
<Button
|
|
215
|
-
|
|
209
|
+
size="sm"
|
|
216
210
|
Icon={icons.FilePlus}
|
|
217
211
|
onclick={() => { handleFileUpload() }}
|
|
218
212
|
>
|
|
@@ -221,7 +215,7 @@
|
|
|
221
215
|
</div>
|
|
222
216
|
</div>
|
|
223
217
|
<!-- file explorer body -->
|
|
224
|
-
<div class="flex flex-1 overflow-auto h-[calc(100vh-
|
|
218
|
+
<div class="flex flex-1 overflow-auto h-[calc(100vh-8rem)]">
|
|
225
219
|
<ContextMenu.Root bind:open={contextMenuOpen}>
|
|
226
220
|
<ContextMenu.Trigger
|
|
227
221
|
class="z-10 flex flex-1 flex-wrap content-start items-start justify-start gap-4 p-4"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
</script>
|
|
30
30
|
|
|
31
|
-
<button onclick={handleClick} class="flex flex-col items-center gap-2 w-full border p-4 rounded-md bg-muted
|
|
31
|
+
<button onclick={handleClick} class="flex flex-col items-center gap-2 w-full border p-4 rounded-md bg-muted">
|
|
32
32
|
{#if value}
|
|
33
33
|
<FileIcon entry={value} {utils} />
|
|
34
34
|
{:else}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExposedServiceOutput, ServiceOp } from "@lobb-js/core";
|
|
2
2
|
|
|
3
3
|
export type StorageFsCreateOne = ServiceOp<
|
|
4
|
-
{ file: File; data?: { path?: string }; force?: boolean },
|
|
4
|
+
{ file: File; data?: { path?: string; parent_id?: number | null }; force?: boolean },
|
|
5
5
|
ExposedServiceOutput
|
|
6
6
|
>;
|
|
7
7
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Context } from "hono";
|
|
2
2
|
import type { Lobb } from "@lobb-js/core";
|
|
3
|
-
import { StorageAdapter } from "./adapters/index.ts";
|
|
4
3
|
import { LobbError } from "@lobb-js/core";
|
|
5
4
|
|
|
6
5
|
export async function getFileFromRequestBody(
|
|
@@ -18,18 +17,49 @@ export async function getFileFromRequestBody(
|
|
|
18
17
|
return undefined;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
// ── parent_id / path helpers ──────────────────────────────────────────────
|
|
21
|
+
//
|
|
22
|
+
// The storage_fs table uses parent_id as the source of truth for the
|
|
23
|
+
// hierarchy. `path` is a derived cache, maintained by the service layer on
|
|
24
|
+
// every write so subtree queries and breadcrumbs stay O(1). These helpers
|
|
25
|
+
// keep both sides in sync — see services.ts for the actual write paths.
|
|
26
|
+
//
|
|
27
|
+
// `path` is the directory the entry lives in (e.g. "/imports/risks/" for a
|
|
28
|
+
// file named "out.csv" placed inside /imports/risks/). The entry's own name
|
|
29
|
+
// is NOT appended to its `path` — that matches the existing convention used
|
|
30
|
+
// by every test in the suite.
|
|
31
|
+
|
|
32
|
+
// Path convention: root is "/", every other directory is "/foo" or
|
|
33
|
+
// "/foo/bar" (no trailing slash). The `path` column stores the parent
|
|
34
|
+
// directory's full path — so an entry at the root has path "/", and an
|
|
35
|
+
// entry inside /foo has path "/foo".
|
|
36
|
+
function buildPath(segments: string[]): string {
|
|
37
|
+
return segments.length === 0 ? "/" : "/" + segments.join("/");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a directory path string to the id of the record representing it.
|
|
42
|
+
* Returns null for "/" (root). Throws BAD_REQUEST if any segment of the
|
|
43
|
+
* path doesn't exist.
|
|
44
|
+
*/
|
|
45
|
+
export async function resolveParentIdFromPath(
|
|
46
|
+
path: string,
|
|
47
|
+
lobb: Lobb,
|
|
48
|
+
): Promise<number | null> {
|
|
49
|
+
const segments = (path ?? "/").split("/").filter(Boolean);
|
|
50
|
+
if (segments.length === 0) return null;
|
|
51
|
+
|
|
52
|
+
let parentId: number | null = null;
|
|
53
|
+
const walked: string[] = [];
|
|
54
|
+
for (const segment of segments) {
|
|
55
|
+
const parentPath = buildPath(walked);
|
|
27
56
|
const directory = (await lobb.collectionStore.findAll({
|
|
28
57
|
collectionName: "storage_fs",
|
|
29
58
|
params: {
|
|
30
59
|
filter: {
|
|
31
|
-
name:
|
|
32
|
-
path:
|
|
60
|
+
name: segment,
|
|
61
|
+
path: parentPath,
|
|
62
|
+
type: "directory",
|
|
33
63
|
},
|
|
34
64
|
},
|
|
35
65
|
})).data[0];
|
|
@@ -40,48 +70,112 @@ export async function validatePathExists(path: string, lobb: Lobb) {
|
|
|
40
70
|
message: `The specified path does not exist. Please create it first.`,
|
|
41
71
|
});
|
|
42
72
|
}
|
|
73
|
+
|
|
74
|
+
parentId = directory.id;
|
|
75
|
+
walked.push(segment);
|
|
43
76
|
}
|
|
77
|
+
|
|
78
|
+
return parentId;
|
|
44
79
|
}
|
|
45
80
|
|
|
46
81
|
/**
|
|
47
|
-
*
|
|
82
|
+
* Like resolveParentIdFromPath, but creates any missing directories along
|
|
83
|
+
* the way (used by force=true uploads).
|
|
48
84
|
*/
|
|
49
|
-
export async function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
85
|
+
export async function ensureParentIdForPath(
|
|
86
|
+
path: string,
|
|
87
|
+
lobb: Lobb,
|
|
88
|
+
): Promise<number | null> {
|
|
89
|
+
const segments = (path ?? "/").split("/").filter(Boolean);
|
|
90
|
+
if (segments.length === 0) return null;
|
|
53
91
|
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
let currentPath = "/";
|
|
59
|
-
for (const dirName of pathArray) {
|
|
60
|
-
// Check if this directory exists
|
|
92
|
+
let parentId: number | null = null;
|
|
93
|
+
const walked: string[] = [];
|
|
94
|
+
for (const segment of segments) {
|
|
95
|
+
const parentPath = buildPath(walked);
|
|
61
96
|
const existing = (await lobb.collectionStore.findAll({
|
|
62
97
|
collectionName: "storage_fs",
|
|
63
98
|
params: {
|
|
64
99
|
filter: {
|
|
65
|
-
name:
|
|
66
|
-
path:
|
|
100
|
+
name: segment,
|
|
101
|
+
path: parentPath,
|
|
67
102
|
type: "directory",
|
|
68
103
|
},
|
|
69
104
|
},
|
|
70
|
-
})).data;
|
|
105
|
+
})).data[0];
|
|
71
106
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
107
|
+
if (existing) {
|
|
108
|
+
parentId = existing.id;
|
|
109
|
+
} else {
|
|
110
|
+
const created = await lobb.collectionStore.createOne({
|
|
75
111
|
collectionName: "storage_fs",
|
|
76
112
|
data: {
|
|
77
|
-
name:
|
|
78
|
-
path:
|
|
113
|
+
name: segment,
|
|
114
|
+
path: parentPath,
|
|
115
|
+
parent_id: parentId,
|
|
79
116
|
type: "directory",
|
|
80
117
|
},
|
|
81
118
|
});
|
|
119
|
+
parentId = created.data.id;
|
|
120
|
+
}
|
|
121
|
+
walked.push(segment);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return parentId;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Compute the path string for an entry whose parent is `parentId`. Walks up
|
|
129
|
+
* the parent chain to the root. Used on insert and whenever a record's
|
|
130
|
+
* parent_id changes.
|
|
131
|
+
*/
|
|
132
|
+
export async function computePathForParent(
|
|
133
|
+
parentId: number | null,
|
|
134
|
+
lobb: Lobb,
|
|
135
|
+
): Promise<string> {
|
|
136
|
+
if (parentId == null) return "/";
|
|
137
|
+
|
|
138
|
+
const segments: string[] = [];
|
|
139
|
+
let currentId: number | null = parentId;
|
|
140
|
+
// Defensive depth cap — guards against accidental cycles. Real hierarchies
|
|
141
|
+
// don't get this deep; if we somehow do, throw rather than infinite-loop.
|
|
142
|
+
for (let depth = 0; depth < 1024; depth++) {
|
|
143
|
+
if (currentId == null) break;
|
|
144
|
+
const row: any = (await lobb.collectionStore.findOne({
|
|
145
|
+
collectionName: "storage_fs",
|
|
146
|
+
id: String(currentId),
|
|
147
|
+
})).data;
|
|
148
|
+
if (!row) {
|
|
149
|
+
throw new LobbError({
|
|
150
|
+
code: "BAD_REQUEST",
|
|
151
|
+
message: `Parent directory ${currentId} not found.`,
|
|
152
|
+
});
|
|
82
153
|
}
|
|
154
|
+
segments.unshift(row.name);
|
|
155
|
+
currentId = row.parent_id ?? null;
|
|
156
|
+
}
|
|
157
|
+
return buildPath(segments);
|
|
158
|
+
}
|
|
83
159
|
|
|
84
|
-
|
|
85
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Return every descendant of `parentId`, breadth-first. Used by cascading
|
|
162
|
+
* deletes and by rename/move to recompute paths for the whole subtree in
|
|
163
|
+
* one pass.
|
|
164
|
+
*/
|
|
165
|
+
export async function findDescendants(
|
|
166
|
+
parentId: number,
|
|
167
|
+
lobb: Lobb,
|
|
168
|
+
): Promise<any[]> {
|
|
169
|
+
const out: any[] = [];
|
|
170
|
+
let frontier: number[] = [parentId];
|
|
171
|
+
while (frontier.length > 0) {
|
|
172
|
+
const children = (await lobb.collectionStore.findAll({
|
|
173
|
+
collectionName: "storage_fs",
|
|
174
|
+
params: { filter: { parent_id: { $in: frontier } } },
|
|
175
|
+
})).data;
|
|
176
|
+
if (children.length === 0) break;
|
|
177
|
+
out.push(...children);
|
|
178
|
+
frontier = children.map((c: any) => c.id);
|
|
86
179
|
}
|
|
180
|
+
return out;
|
|
87
181
|
}
|