@vryzel/file-next 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -3
- package/dist/{filesystem-D1Jjx3md.d.ts → filesystem-B0bJh8bW.d.ts} +11 -0
- package/dist/{filesystem-BbZJLy4A.d.cts → filesystem-CewcaWEl.d.cts} +11 -0
- package/dist/index.cjs +137 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +137 -2
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +14 -0
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +3 -2
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.js +14 -0
- package/dist/server/index.js.map +1 -1
- package/dist/sync/index.cjs +20 -0
- package/dist/sync/index.cjs.map +1 -1
- package/dist/sync/index.d.cts +5 -1
- package/dist/sync/index.d.ts +5 -1
- package/dist/sync/index.js +20 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/{with-auth-BueIpNZ7.d.cts → with-auth-DDTngJoQ.d.cts} +1 -1
- package/dist/{with-auth-yfezt7YS.d.ts → with-auth-f82V2WB6.d.ts} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
# @vryzel/file-next
|
|
2
2
|
|
|
3
|
-
S3/R2 filesystem for Next.js
|
|
3
|
+
S3/R2 filesystem for Next.js. Bytes in your bucket. The folder tree in **your** SQLite or Postgres. Tenant comes from the server, never from the client.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
pnpm add @vryzel/file-next
|
|
7
|
+
# optional, depending on the store:
|
|
8
|
+
pnpm add better-sqlite3 # SQLite
|
|
9
|
+
pnpm add pg # Postgres
|
|
7
10
|
```
|
|
8
11
|
|
|
12
|
+
Peer deps `better-sqlite3` and `pg` are optional. Install only the one you use.
|
|
13
|
+
|
|
14
|
+
## Quick path
|
|
15
|
+
|
|
9
16
|
```ts
|
|
17
|
+
// app/lib/file-next.ts
|
|
10
18
|
import {
|
|
11
19
|
asTenantId,
|
|
12
20
|
asUserId,
|
|
@@ -29,6 +37,7 @@ const fs = createFileSystem(
|
|
|
29
37
|
},
|
|
30
38
|
{ store },
|
|
31
39
|
);
|
|
40
|
+
|
|
32
41
|
export const actions = createServerActions({
|
|
33
42
|
store,
|
|
34
43
|
fs,
|
|
@@ -40,6 +49,177 @@ export const actions = createServerActions({
|
|
|
40
49
|
});
|
|
41
50
|
```
|
|
42
51
|
|
|
43
|
-
|
|
52
|
+
```ts
|
|
53
|
+
// app/lib/actions.ts
|
|
54
|
+
"use server";
|
|
55
|
+
import { actions } from "./file-next";
|
|
56
|
+
|
|
57
|
+
export const listFiles = (input: { parentId: string | null; cursor?: string; limit?: number }) =>
|
|
58
|
+
actions.listFiles(input);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Replace `getAuth` with Clerk / Auth.js / your session. Import `@vryzel/file-next/server` only from server modules.
|
|
62
|
+
|
|
63
|
+
## Use cases
|
|
64
|
+
|
|
65
|
+
### 1. SQLite (one process)
|
|
66
|
+
|
|
67
|
+
Default for a single Next.js server.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { createSqliteStore } from "@vryzel/file-next";
|
|
71
|
+
const store = createSqliteStore({ path: ".data/metadata.db" });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Schema is created on first call. Path is relative to `process.cwd()`.
|
|
75
|
+
|
|
76
|
+
### 2. Postgres (multi-instance + RLS)
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { createPostgresStore } from "@vryzel/file-next";
|
|
80
|
+
const store = createPostgresStore({
|
|
81
|
+
connectionString: process.env.DATABASE_URL!,
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Install `pg`. Tenant isolation uses `SET LOCAL app.current_tenant` + row-level security.
|
|
86
|
+
|
|
87
|
+
### 3. In-memory (tests / Storybook)
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { createMemoryStore, createMemoryFileSystem } from "@vryzel/file-next";
|
|
91
|
+
|
|
92
|
+
const store = createMemoryStore();
|
|
93
|
+
const fs = createMemoryFileSystem({ store });
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
No AWS. Data dies with the process.
|
|
97
|
+
|
|
98
|
+
### 4. Cloudflare R2
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const fs = createFileSystem(
|
|
102
|
+
{
|
|
103
|
+
provider: "r2",
|
|
104
|
+
bucket: process.env.FILE_NEXT_BUCKET!,
|
|
105
|
+
endpoint: process.env.FILE_NEXT_ENDPOINT!, // https://<accountid>.r2.cloudflarestorage.com
|
|
106
|
+
credentials: {
|
|
107
|
+
accessKeyId: process.env.FILE_NEXT_ACCESS_KEY_ID!,
|
|
108
|
+
secretAccessKey: process.env.FILE_NEXT_SECRET_ACCESS_KEY!,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{ store },
|
|
112
|
+
);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
R2 is S3-compatible. Set bucket CORS for `PUT`/`GET` from your origin.
|
|
116
|
+
|
|
117
|
+
### 5. Env singleton
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { getFileSystem } from "@vryzel/file-next";
|
|
121
|
+
const fs = getFileSystem(); // memoized for the process
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Reads:
|
|
125
|
+
|
|
126
|
+
| Var | Required |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `FILE_NEXT_PROVIDER` | `s3` or `r2` |
|
|
129
|
+
| `FILE_NEXT_BUCKET` | yes |
|
|
130
|
+
| `FILE_NEXT_REGION` | S3 |
|
|
131
|
+
| `FILE_NEXT_ENDPOINT` | R2 |
|
|
132
|
+
| `FILE_NEXT_ACCESS_KEY_ID` | yes |
|
|
133
|
+
| `FILE_NEXT_SECRET_ACCESS_KEY` | yes |
|
|
134
|
+
| `FILE_NEXT_FORCE_PATH_STYLE` | optional |
|
|
135
|
+
|
|
136
|
+
Missing config throws `FileSystemError` (not retryable).
|
|
137
|
+
|
|
138
|
+
### 6. Multi-tenant keys
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const scoped = fs.forTenant("acme");
|
|
142
|
+
// object keys are prefixed t/acme/{nodeId} unless prefixTenantKeys: false
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Every store method also takes `tenantId`. Do not take tenant from the request body.
|
|
146
|
+
|
|
147
|
+
### 7. Server actions
|
|
148
|
+
|
|
149
|
+
`createServerActions` returns:
|
|
150
|
+
|
|
151
|
+
`listFiles`, `searchFiles`, `listTrash`, `createFolder`, `deleteFile`, `moveFile`, `copyFile`, `setMetadata`, `prepareUpload`, `confirmUpload`, `restoreNode`, `createShare`, `resolveShare`, `revokeShare`.
|
|
152
|
+
|
|
153
|
+
All return `Result<T, FileSystemError>`: `{ ok: true, value }` or `{ ok: false, error }`.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const listed = await actions.listFiles({ parentId: null, limit: 50 });
|
|
157
|
+
if (!listed.ok) throw listed.error;
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`listFiles` / `listTrash` accept `cursor` + `limit`.
|
|
161
|
+
|
|
162
|
+
### 8. Route handlers (upload / download)
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
// app/api/upload/route.ts
|
|
166
|
+
import { createUploadRouteHandler } from "@vryzel/file-next/server";
|
|
167
|
+
import { getFileSystem } from "@vryzel/file-next";
|
|
168
|
+
|
|
169
|
+
export const PUT = createUploadRouteHandler({
|
|
170
|
+
fs: getFileSystem(),
|
|
171
|
+
maxBytes: 25 * 1024 * 1024,
|
|
172
|
+
allowedContentTypes: ["image/*", "application/pdf"],
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Many apps skip this and PUT to a signed URL / a thin `writeThroughFile` route (see the test app). Use whichever matches your upload flow.
|
|
177
|
+
|
|
178
|
+
### 9. Write-through (object first, then the tree)
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { createWriteThrough } from "@vryzel/file-next/sync";
|
|
182
|
+
|
|
183
|
+
const writeThrough = createWriteThrough(fs, store);
|
|
184
|
+
await writeThrough.writeThroughFile({
|
|
185
|
+
tenantId,
|
|
186
|
+
parentId: null,
|
|
187
|
+
name: "photo.png",
|
|
188
|
+
body,
|
|
189
|
+
contentType: "image/png",
|
|
190
|
+
ownerId,
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
If the metadata insert fails after the object is written, the key lands in `pending_orphans`. `reconcile()` drains them.
|
|
195
|
+
|
|
196
|
+
### 10. Result + errors
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { ok, err, unwrap, type Result } from "@vryzel/file-next";
|
|
200
|
+
import { FileSystemError } from "@vryzel/file-next/errors";
|
|
201
|
+
|
|
202
|
+
if (!result.ok) {
|
|
203
|
+
if (result.error.code === "Conflict") { /* duplicate name */ }
|
|
204
|
+
if (result.error.retryable) { /* backoff */ }
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`@vryzel/file-next/errors` is the client-safe entry (no Node builtins).
|
|
209
|
+
|
|
210
|
+
## Imports
|
|
211
|
+
|
|
212
|
+
| Entry | Use from |
|
|
213
|
+
|---|---|
|
|
214
|
+
| `@vryzel/file-next` | server or shared types |
|
|
215
|
+
| `@vryzel/file-next/server` | Server Actions / Route Handlers only (`server-only`) |
|
|
216
|
+
| `@vryzel/file-next/sync` | server |
|
|
217
|
+
| `@vryzel/file-next/errors` | client or server |
|
|
218
|
+
|
|
219
|
+
## Not this package
|
|
220
|
+
|
|
221
|
+
- React hooks → `@vryzel/file-next-headless`
|
|
222
|
+
- Ready-made explorer → `@vryzel/file-next-ui`
|
|
223
|
+
- CLI → `@vryzel/file-next-cli`
|
|
44
224
|
|
|
45
|
-
|
|
225
|
+
Bucket CORS and IAM: [provider-setup](https://github.com/Vryzel/file-next/blob/main/docs/provider-setup.md), [security](https://github.com/Vryzel/file-next/blob/main/docs/security.md).
|
|
@@ -724,6 +724,17 @@ interface MetadataStore {
|
|
|
724
724
|
* tombstones the entire subtree in one call.
|
|
725
725
|
*/
|
|
726
726
|
deleteNode(input: DeleteNodeInput): Promise<Result<void, FileSystemError>>;
|
|
727
|
+
/**
|
|
728
|
+
* Hard-delete a trashed node and its trashed descendants.
|
|
729
|
+
* Live nodes return NotFound. Returns S3 keys of files removed
|
|
730
|
+
* so the caller can delete objects.
|
|
731
|
+
*/
|
|
732
|
+
purgeNode(input: {
|
|
733
|
+
tenantId: TenantId;
|
|
734
|
+
id: string;
|
|
735
|
+
}): Promise<Result<{
|
|
736
|
+
s3Keys: ReadonlyArray<string>;
|
|
737
|
+
}, FileSystemError>>;
|
|
727
738
|
/** Update user metadata (merge or replace). */
|
|
728
739
|
updateMetadata(input: UpdateMetadataInput): Promise<Result<FileNode, FileSystemError>>;
|
|
729
740
|
/** Case-insensitive name search within an optional folder scope. */
|
|
@@ -724,6 +724,17 @@ interface MetadataStore {
|
|
|
724
724
|
* tombstones the entire subtree in one call.
|
|
725
725
|
*/
|
|
726
726
|
deleteNode(input: DeleteNodeInput): Promise<Result<void, FileSystemError>>;
|
|
727
|
+
/**
|
|
728
|
+
* Hard-delete a trashed node and its trashed descendants.
|
|
729
|
+
* Live nodes return NotFound. Returns S3 keys of files removed
|
|
730
|
+
* so the caller can delete objects.
|
|
731
|
+
*/
|
|
732
|
+
purgeNode(input: {
|
|
733
|
+
tenantId: TenantId;
|
|
734
|
+
id: string;
|
|
735
|
+
}): Promise<Result<{
|
|
736
|
+
s3Keys: ReadonlyArray<string>;
|
|
737
|
+
}, FileSystemError>>;
|
|
727
738
|
/** Update user metadata (merge or replace). */
|
|
728
739
|
updateMetadata(input: UpdateMetadataInput): Promise<Result<FileNode, FileSystemError>>;
|
|
729
740
|
/** Case-insensitive name search within an optional folder scope. */
|
package/dist/index.cjs
CHANGED
|
@@ -3976,6 +3976,36 @@ var createMemoryStore = () => {
|
|
|
3976
3976
|
}
|
|
3977
3977
|
return ok(void 0);
|
|
3978
3978
|
},
|
|
3979
|
+
async purgeNode(input) {
|
|
3980
|
+
const n = nodes.get(input.id);
|
|
3981
|
+
if (!n || n.tenantId !== input.tenantId || n.deletedAt === null) {
|
|
3982
|
+
return err(
|
|
3983
|
+
new FileSystemError({
|
|
3984
|
+
code: "NotFound",
|
|
3985
|
+
message: `Trashed node ${input.id} not found`,
|
|
3986
|
+
retryable: false
|
|
3987
|
+
})
|
|
3988
|
+
);
|
|
3989
|
+
}
|
|
3990
|
+
const prefix = `${n.path}/`;
|
|
3991
|
+
const s3Keys = [];
|
|
3992
|
+
const ids = [];
|
|
3993
|
+
for (const node of nodes.values()) {
|
|
3994
|
+
if (node.tenantId !== input.tenantId || node.deletedAt === null) continue;
|
|
3995
|
+
if (node.id !== n.id && !node.path.startsWith(prefix)) continue;
|
|
3996
|
+
ids.push(node.id);
|
|
3997
|
+
if (node.kind === "file" && node.s3Key) s3Keys.push(node.s3Key);
|
|
3998
|
+
}
|
|
3999
|
+
for (const id of ids) {
|
|
4000
|
+
const node = nodes.get(id);
|
|
4001
|
+
if (node) removeFromChildrenIndex(node);
|
|
4002
|
+
nodes.delete(id);
|
|
4003
|
+
}
|
|
4004
|
+
for (const [token, share] of shares) {
|
|
4005
|
+
if (ids.includes(share.nodeId)) shares.delete(token);
|
|
4006
|
+
}
|
|
4007
|
+
return ok({ s3Keys });
|
|
4008
|
+
},
|
|
3979
4009
|
async updateMetadata(input) {
|
|
3980
4010
|
const n = nodes.get(input.id);
|
|
3981
4011
|
if (!n || n.tenantId !== input.tenantId || n.deletedAt !== null) {
|
|
@@ -4206,7 +4236,7 @@ var rowToFileNode = (row) => ({
|
|
|
4206
4236
|
updatedAt: new Date(row.updated_at),
|
|
4207
4237
|
deletedAt: row.deleted_at === null ? null : new Date(row.deleted_at)
|
|
4208
4238
|
});
|
|
4209
|
-
var SCHEMA_VERSION =
|
|
4239
|
+
var SCHEMA_VERSION = 4;
|
|
4210
4240
|
var MIGRATION_SQL = `
|
|
4211
4241
|
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
4212
4242
|
key TEXT PRIMARY KEY,
|
|
@@ -4290,7 +4320,8 @@ WHEN new.deleted_at IS NULL BEGIN
|
|
|
4290
4320
|
INSERT INTO nodes_fts(rowid, name) VALUES (new.rowid, new.name);
|
|
4291
4321
|
END;
|
|
4292
4322
|
|
|
4293
|
-
CREATE TRIGGER IF NOT EXISTS nodes_fts_ad AFTER DELETE ON nodes
|
|
4323
|
+
CREATE TRIGGER IF NOT EXISTS nodes_fts_ad AFTER DELETE ON nodes
|
|
4324
|
+
WHEN old.deleted_at IS NULL BEGIN
|
|
4294
4325
|
INSERT INTO nodes_fts(nodes_fts, rowid, name) VALUES('delete', old.rowid, old.name);
|
|
4295
4326
|
END;
|
|
4296
4327
|
|
|
@@ -4300,8 +4331,39 @@ CREATE TRIGGER IF NOT EXISTS nodes_fts_au AFTER UPDATE ON nodes BEGIN
|
|
|
4300
4331
|
SELECT new.rowid, new.name WHERE new.deleted_at IS NULL;
|
|
4301
4332
|
END;
|
|
4302
4333
|
`;
|
|
4334
|
+
var FTS_REBUILD_SQL = `
|
|
4335
|
+
DROP TRIGGER IF EXISTS nodes_fts_ai;
|
|
4336
|
+
DROP TRIGGER IF EXISTS nodes_fts_ad;
|
|
4337
|
+
DROP TRIGGER IF EXISTS nodes_fts_au;
|
|
4338
|
+
DROP TABLE IF EXISTS nodes_fts;
|
|
4339
|
+
CREATE VIRTUAL TABLE nodes_fts USING fts5(
|
|
4340
|
+
name,
|
|
4341
|
+
content='nodes',
|
|
4342
|
+
content_rowid='rowid',
|
|
4343
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
4344
|
+
);
|
|
4345
|
+
CREATE TRIGGER nodes_fts_ai AFTER INSERT ON nodes
|
|
4346
|
+
WHEN new.deleted_at IS NULL BEGIN
|
|
4347
|
+
INSERT INTO nodes_fts(rowid, name) VALUES (new.rowid, new.name);
|
|
4348
|
+
END;
|
|
4349
|
+
CREATE TRIGGER nodes_fts_ad AFTER DELETE ON nodes
|
|
4350
|
+
WHEN old.deleted_at IS NULL BEGIN
|
|
4351
|
+
INSERT INTO nodes_fts(nodes_fts, rowid, name) VALUES('delete', old.rowid, old.name);
|
|
4352
|
+
END;
|
|
4353
|
+
CREATE TRIGGER nodes_fts_au AFTER UPDATE ON nodes BEGIN
|
|
4354
|
+
INSERT INTO nodes_fts(nodes_fts, rowid, name) VALUES('delete', old.rowid, old.name);
|
|
4355
|
+
INSERT INTO nodes_fts(rowid, name)
|
|
4356
|
+
SELECT new.rowid, new.name WHERE new.deleted_at IS NULL;
|
|
4357
|
+
END;
|
|
4358
|
+
INSERT INTO nodes_fts(rowid, name) SELECT rowid, name FROM nodes WHERE deleted_at IS NULL;
|
|
4359
|
+
`;
|
|
4303
4360
|
var runMigrations = (db) => {
|
|
4304
4361
|
db.exec(MIGRATION_SQL);
|
|
4362
|
+
const row = db.prepare("SELECT value FROM schema_meta WHERE key = 'version'").get();
|
|
4363
|
+
const version = row ? Number(row.value) : 0;
|
|
4364
|
+
if (!Number.isFinite(version) || version < 4) {
|
|
4365
|
+
db.exec(FTS_REBUILD_SQL);
|
|
4366
|
+
}
|
|
4305
4367
|
db.prepare(
|
|
4306
4368
|
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)"
|
|
4307
4369
|
).run(String(SCHEMA_VERSION));
|
|
@@ -4346,6 +4408,7 @@ var createSqliteStore = (options) => {
|
|
|
4346
4408
|
listChildren: wrap((s, i) => s.listChildren(i)),
|
|
4347
4409
|
moveNode: wrap((s, i) => s.moveNode(i)),
|
|
4348
4410
|
deleteNode: wrap((s, i) => s.deleteNode(i)),
|
|
4411
|
+
purgeNode: wrap((s, i) => s.purgeNode(i)),
|
|
4349
4412
|
updateMetadata: wrap((s, i) => s.updateMetadata(i)),
|
|
4350
4413
|
search: wrap((s, i) => s.search(i)),
|
|
4351
4414
|
getPath: wrap((s, i) => s.getPath(i)),
|
|
@@ -4685,6 +4748,35 @@ var buildInner = (Database, options) => {
|
|
|
4685
4748
|
}
|
|
4686
4749
|
return ok(void 0);
|
|
4687
4750
|
},
|
|
4751
|
+
async purgeNode(input) {
|
|
4752
|
+
const node = db.prepare(
|
|
4753
|
+
`SELECT id, path FROM nodes WHERE id = ? AND tenant_id = ? AND deleted_at IS NOT NULL`
|
|
4754
|
+
).get(input.id, input.tenantId);
|
|
4755
|
+
if (!node) {
|
|
4756
|
+
return err(
|
|
4757
|
+
new FileSystemError({
|
|
4758
|
+
code: "NotFound",
|
|
4759
|
+
message: `Trashed node ${input.id} not found`,
|
|
4760
|
+
retryable: false
|
|
4761
|
+
})
|
|
4762
|
+
);
|
|
4763
|
+
}
|
|
4764
|
+
const like = `${node.path}/%`;
|
|
4765
|
+
const files = db.prepare(
|
|
4766
|
+
`SELECT s3_key FROM nodes
|
|
4767
|
+
WHERE tenant_id = ? AND deleted_at IS NOT NULL AND kind = 'file' AND s3_key != ''
|
|
4768
|
+
AND (id = ? OR path LIKE ?)`
|
|
4769
|
+
).all(input.tenantId, input.id, like);
|
|
4770
|
+
db.prepare(
|
|
4771
|
+
`DELETE FROM shares WHERE tenant_id = ? AND node_id IN (
|
|
4772
|
+
SELECT id FROM nodes WHERE tenant_id = ? AND deleted_at IS NOT NULL AND (id = ? OR path LIKE ?)
|
|
4773
|
+
)`
|
|
4774
|
+
).run(input.tenantId, input.tenantId, input.id, like);
|
|
4775
|
+
db.prepare(
|
|
4776
|
+
`DELETE FROM nodes WHERE tenant_id = ? AND deleted_at IS NOT NULL AND (id = ? OR path LIKE ?)`
|
|
4777
|
+
).run(input.tenantId, input.id, like);
|
|
4778
|
+
return ok({ s3Keys: files.map((row) => row.s3_key) });
|
|
4779
|
+
},
|
|
4688
4780
|
async updateMetadata(input) {
|
|
4689
4781
|
const node = stmtGetById.get(input.id, input.tenantId);
|
|
4690
4782
|
if (!node) return notFound(input.id);
|
|
@@ -5065,6 +5157,7 @@ var createPostgresStore = (options) => {
|
|
|
5065
5157
|
listChildren: wrap((s, i) => s.listChildren(i)),
|
|
5066
5158
|
moveNode: wrap((s, i) => s.moveNode(i)),
|
|
5067
5159
|
deleteNode: wrap((s, i) => s.deleteNode(i)),
|
|
5160
|
+
purgeNode: wrap((s, i) => s.purgeNode(i)),
|
|
5068
5161
|
updateMetadata: wrap((s, i) => s.updateMetadata(i)),
|
|
5069
5162
|
search: wrap((s, i) => s.search(i)),
|
|
5070
5163
|
getPath: wrap((s, i) => s.getPath(i)),
|
|
@@ -5405,6 +5498,48 @@ var buildInner2 = (pg, options) => {
|
|
|
5405
5498
|
}
|
|
5406
5499
|
return ok(void 0);
|
|
5407
5500
|
},
|
|
5501
|
+
async purgeNode(input) {
|
|
5502
|
+
await ensureInitialized();
|
|
5503
|
+
const nodeRes = await q(
|
|
5504
|
+
input.tenantId,
|
|
5505
|
+
`SELECT id, path FROM "${schema}".nodes
|
|
5506
|
+
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NOT NULL`,
|
|
5507
|
+
[input.id, input.tenantId]
|
|
5508
|
+
);
|
|
5509
|
+
const node = nodeRes.rows[0];
|
|
5510
|
+
if (!node) {
|
|
5511
|
+
return err(
|
|
5512
|
+
new FileSystemError({
|
|
5513
|
+
code: "NotFound",
|
|
5514
|
+
message: `Trashed node ${input.id} not found`,
|
|
5515
|
+
retryable: false
|
|
5516
|
+
})
|
|
5517
|
+
);
|
|
5518
|
+
}
|
|
5519
|
+
const like = `${node.path}/%`;
|
|
5520
|
+
const files = await q(
|
|
5521
|
+
input.tenantId,
|
|
5522
|
+
`SELECT s3_key FROM "${schema}".nodes
|
|
5523
|
+
WHERE tenant_id = $1 AND deleted_at IS NOT NULL AND kind = 'file' AND s3_key <> ''
|
|
5524
|
+
AND (id = $2 OR path LIKE $3)`,
|
|
5525
|
+
[input.tenantId, input.id, like]
|
|
5526
|
+
);
|
|
5527
|
+
await q(
|
|
5528
|
+
input.tenantId,
|
|
5529
|
+
`DELETE FROM "${schema}".shares WHERE tenant_id = $1 AND node_id IN (
|
|
5530
|
+
SELECT id FROM "${schema}".nodes
|
|
5531
|
+
WHERE tenant_id = $1 AND deleted_at IS NOT NULL AND (id = $2 OR path LIKE $3)
|
|
5532
|
+
)`,
|
|
5533
|
+
[input.tenantId, input.id, like]
|
|
5534
|
+
);
|
|
5535
|
+
await q(
|
|
5536
|
+
input.tenantId,
|
|
5537
|
+
`DELETE FROM "${schema}".nodes
|
|
5538
|
+
WHERE tenant_id = $1 AND deleted_at IS NOT NULL AND (id = $2 OR path LIKE $3)`,
|
|
5539
|
+
[input.tenantId, input.id, like]
|
|
5540
|
+
);
|
|
5541
|
+
return ok({ s3Keys: files.rows.map((row) => row.s3_key) });
|
|
5542
|
+
},
|
|
5408
5543
|
async updateMetadata(input) {
|
|
5409
5544
|
await ensureInitialized();
|
|
5410
5545
|
const exists = await q(
|