@vryzel/file-next 0.4.1 → 0.4.3

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 CHANGED
@@ -159,6 +159,16 @@ if (!listed.ok) throw listed.error;
159
159
 
160
160
  `listFiles` / `listTrash` accept `cursor` + `limit`.
161
161
 
162
+ `createShare` returns `{ token, url }` where `url` is `/api/share/{token}` (your domain, not the bucket). Mount:
163
+
164
+ ```ts
165
+ // app/api/share/[token]/route.ts
166
+ import { createShareRouteHandler } from "@vryzel/file-next/server";
167
+ export const GET = createShareRouteHandler({ store, fs });
168
+ ```
169
+
170
+ The handler streams the object through Next.js. Folders cannot be shared this way. Optional `sharePathPrefix` on `createServerActions` (default `/api/share`).
171
+
162
172
  ### 8. Route handlers (upload / download)
163
173
 
164
174
  ```ts
@@ -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 = 3;
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 BEGIN
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(