From 39073ceaa49b37c34e041eda7f18406f9cd0b566 Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 21 Jun 2023 10:12:36 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20setup=20NodeCrud=20imple?= =?UTF-8?q?mentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crud/__tests__/testCrudfs.ts | 313 +++++++++++++++++++++ src/{fsa-to-crud => crud}/util.ts | 2 +- src/fsa-to-crud/FsaCrud.ts | 2 +- src/fsa-to-crud/__tests__/FsaCrud.test.ts | 306 +------------------- src/node-to-crud/NodeCrud.ts | 172 +++++++++++ src/node-to-crud/__tests__/FsaCrud.test.ts | 17 ++ 6 files changed, 506 insertions(+), 306 deletions(-) create mode 100644 src/crud/__tests__/testCrudfs.ts rename src/{fsa-to-crud => crud}/util.ts (83%) create mode 100644 src/node-to-crud/NodeCrud.ts create mode 100644 src/node-to-crud/__tests__/FsaCrud.test.ts diff --git a/src/crud/__tests__/testCrudfs.ts b/src/crud/__tests__/testCrudfs.ts new file mode 100644 index 000000000..b8df27a87 --- /dev/null +++ b/src/crud/__tests__/testCrudfs.ts @@ -0,0 +1,313 @@ +import { of } from 'thingies'; +import type {CrudApi} from '../types'; + +export type Setup = () => { + crud: CrudApi; + snapshot: () => Record; +}; + +const b = (str: string) => { + const buf = Buffer.from(str); + return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); +}; + +export const testCrudfs = (setup: Setup) => { + describe('.put()', () => { + test('throws if the type is not valid', async () => { + const { crud } = setup(); + const [, err] = await of(crud.put(['.', 'foo'], 'bar', new Uint8Array())); + expect(err).toBeInstanceOf(TypeError); + expect((err).message).toBe("Failed to execute 'put' on 'crudfs': Name is not allowed."); + }); + + test('throws if id is not valid', async () => { + const { crud } = setup(); + const [, err] = await of(crud.put(['foo'], '..', new Uint8Array())); + expect(err).toBeInstanceOf(TypeError); + expect((err).message).toBe("Failed to execute 'put' on 'crudfs': Name is not allowed."); + }); + + test('can store a resource at root', async () => { + const { crud, snapshot } = setup(); + await crud.put([], 'bar', b('abc')); + expect(snapshot()).toStrictEqual({ + '/bar': 'abc', + }); + }); + + test('can store a resource in two levels deep collection', async () => { + const { crud, snapshot } = setup(); + await crud.put(['a', 'b'], 'bar', b('abc')); + expect(snapshot()).toStrictEqual({ + '/a/b/bar': 'abc', + }); + }); + + test('can overwrite existing resource', async () => { + const { crud, snapshot } = setup(); + await crud.put(['a', 'b'], 'bar', b('abc')); + await crud.put(['a', 'b'], 'bar', b('efg')); + expect(snapshot()).toStrictEqual({ + '/a/b/bar': 'efg', + }); + }); + + test('can choose to throw if item already exists', async () => { + const { crud } = setup(); + await crud.put(['a', 'b'], 'bar', b('abc'), { throwIf: 'exists' }); + const [, err] = await of(crud.put(['a', 'b'], 'bar', b('efg'), { throwIf: 'exists' })); + expect(err).toBeInstanceOf(DOMException); + expect((err).name).toBe('Exists'); + }); + + test('can choose to throw if item does not exist', async () => { + const { crud, snapshot } = setup(); + const [, err] = await of(crud.put(['a', 'b'], 'bar', b('1'), { throwIf: 'missing' })); + await crud.put(['a', 'b'], 'bar', b('2')); + await crud.put(['a', 'b'], 'bar', b('3'), { throwIf: 'missing' }); + expect(err).toBeInstanceOf(DOMException); + expect((err).name).toBe('Missing'); + expect(snapshot()).toStrictEqual({ + '/a/b/bar': '3', + }); + }); + }); + + describe('.get()', () => { + test('throws if the type is not valid', async () => { + const { crud } = setup(); + const [, err] = await of(crud.get(['', 'foo'], 'bar')); + expect(err).toBeInstanceOf(TypeError); + expect((err).message).toBe("Failed to execute 'get' on 'crudfs': Name is not allowed."); + }); + + test('throws if id is not valid', async () => { + const { crud } = setup(); + const [, err] = await of(crud.get(['foo'], '')); + expect(err).toBeInstanceOf(TypeError); + expect((err).message).toBe("Failed to execute 'get' on 'crudfs': Name is not allowed."); + }); + + test('throws if collection does not exist', async () => { + const { crud } = setup(); + const [, err] = await of(crud.get(['foo'], 'bar')); + expect(err).toBeInstanceOf(DOMException); + expect((err).name).toBe('CollectionNotFound'); + }); + + test('throws if resource does not exist', async () => { + const { crud } = setup(); + await crud.put(['foo'], 'bar', b('abc')); + const [, err] = await of(crud.get(['foo'], 'baz')); + expect(err).toBeInstanceOf(DOMException); + expect((err).name).toBe('ResourceNotFound'); + }); + + test('can fetch an existing resource', async () => { + const { crud } = setup(); + await crud.put(['foo'], 'bar', b('abc')); + const blob = await crud.get(['foo'], 'bar'); + expect(blob).toStrictEqual(b('abc')); + }); + }); + + describe('.del()', () => { + test('throws if the type is not valid', async () => { + const { crud } = setup(); + const [, err] = await of(crud.del(['sdf\\dd', 'foo'], 'bar')); + expect(err).toBeInstanceOf(TypeError); + expect((err).message).toBe("Failed to execute 'del' on 'crudfs': Name is not allowed."); + }); + + test('throws if id is not valid', async () => { + const { crud } = setup(); + const [, err] = await of(crud.del(['foo'], 'asdf/asdf')); + expect(err).toBeInstanceOf(TypeError); + expect((err).message).toBe("Failed to execute 'del' on 'crudfs': Name is not allowed."); + }); + + describe('when collection does not exist', () => { + test('throws by default', async () => { + const { crud } = setup(); + const [, err] = await of(crud.del(['foo'], 'bar')); + expect(err).toBeInstanceOf(DOMException); + expect((err).name).toBe('CollectionNotFound'); + }); + + test('does not throw when "silent" flag set', async () => { + const { crud } = setup(); + await crud.del(['foo'], 'bar', true); + }); + }); + + describe('when collection is found but resource is not', () => { + test('throws by default', async () => { + const { crud } = setup(); + await crud.put(['foo'], 'bar', b('abc')); + const [, err] = await of(crud.del(['foo'], 'baz')); + expect(err).toBeInstanceOf(DOMException); + expect((err).name).toBe('ResourceNotFound'); + }); + + test('does not throw when "silent" flag set', async () => { + const { crud } = setup(); + await crud.put(['foo'], 'bar', b('abc')); + await crud.del(['foo'], 'baz', true); + }); + }); + + test('deletes an existing resource', async () => { + const { crud } = setup(); + await crud.put(['foo'], 'bar', b('abc')); + await crud.get(['foo'], 'bar'); + await crud.del(['foo'], 'bar'); + const [, err] = await of(crud.get(['foo'], 'bar')); + expect(err).toBeInstanceOf(DOMException); + expect((err).name).toBe('ResourceNotFound'); + }); + }); + + describe('.info()', () => { + test('throws if the type is not valid', async () => { + const { crud } = setup(); + const [, err] = await of(crud.info(['', 'foo'], 'bar')); + expect(err).toBeInstanceOf(TypeError); + expect((err).message).toBe("Failed to execute 'info' on 'crudfs': Name is not allowed."); + }); + + test('throws if id is not valid', async () => { + const { crud } = setup(); + const [, err] = await of(crud.info(['foo'], '/')); + expect(err).toBeInstanceOf(TypeError); + expect((err).message).toBe("Failed to execute 'info' on 'crudfs': Name is not allowed."); + }); + + test('can retrieve information about a resource', async () => { + const { crud } = setup(); + await crud.put(['foo'], 'bar', b('abc')); + const info = await crud.info(['foo'], 'bar'); + expect(info).toMatchObject({ + type: 'resource', + id: 'bar', + size: 3, + modified: expect.any(Number), + }); + }); + + test('can retrieve information about a collection', async () => { + const { crud } = setup(); + await crud.put(['foo'], 'bar', b('abc')); + const info = await crud.info(['foo']); + expect(info).toMatchObject({ + type: 'collection', + }); + }); + + test('throws when resource not found', async () => { + const { crud } = setup(); + await crud.put(['foo'], 'bar', b('abc')); + const [, err] = await of(crud.info(['foo'], 'baz')); + expect(err).toBeInstanceOf(DOMException); + expect((err).name).toBe('ResourceNotFound'); + }); + + test('throws when collection not found', async () => { + const { crud } = setup(); + await crud.put(['foo', 'a'], 'bar', b('abc')); + const [, err] = await of(crud.info(['foo', 'b'], 'baz')); + expect(err).toBeInstanceOf(DOMException); + expect((err).name).toBe('CollectionNotFound'); + }); + }); + + describe('.drop()', () => { + test('throws if the collection is not valid', async () => { + const { crud } = setup(); + const [, err] = await of(crud.drop(['', 'foo'])); + expect(err).toBeInstanceOf(TypeError); + expect((err).message).toBe("Failed to execute 'drop' on 'crudfs': Name is not allowed."); + }); + + test('can recursively delete a collection', async () => { + const { crud } = setup(); + await crud.put(['foo', 'a'], 'bar', b('1')); + await crud.put(['foo', 'a'], 'baz', b('2')); + await crud.put(['foo', 'b'], 'xyz', b('3')); + const info = await crud.info(['foo', 'a']); + expect(info.type).toBe('collection'); + await crud.drop(['foo', 'a']); + const [, err] = await of(crud.info(['foo', 'a'])); + expect(err).toBeInstanceOf(DOMException); + expect((err).name).toBe('CollectionNotFound'); + }); + + test('throws if collection does not exist', async () => { + const { crud } = setup(); + await crud.put(['foo', 'a'], 'bar', b('1')); + await crud.put(['foo', 'a'], 'baz', b('2')); + await crud.put(['foo', 'b'], 'xyz', b('3')); + const [, err] = await of(crud.drop(['gg'])); + expect(err).toBeInstanceOf(DOMException); + expect((err).name).toBe('CollectionNotFound'); + }); + + test('when "silent" flag set, does not throw if collection does not exist', async () => { + const { crud } = setup(); + await crud.put(['foo', 'a'], 'bar', b('1')); + await crud.put(['foo', 'a'], 'baz', b('2')); + await crud.put(['foo', 'b'], 'xyz', b('3')); + await crud.drop(['gg'], true); + }); + + test('can recursively delete everything from root', async () => { + const { crud, snapshot } = setup(); + await crud.put(['foo', 'a'], 'bar', b('1')); + await crud.put(['baz', 'a'], 'baz', b('2')); + await crud.put(['bar', 'b'], 'xyz', b('3')); + const info = await crud.info(['foo', 'a']); + expect(info.type).toBe('collection'); + await crud.drop([]); + expect(snapshot()).toEqual({}); + }); + }); + + describe('.list()', () => { + test('throws if the collection is not valid', async () => { + const { crud } = setup(); + const [, err] = await of(crud.list(['./..', 'foo'])); + expect(err).toBeInstanceOf(TypeError); + expect((err).message).toBe("Failed to execute 'drop' on 'crudfs': Name is not allowed."); + }); + + test('can retrieve a list of resources and collections at root', async () => { + const { crud } = setup(); + await crud.put(['foo'], 'bar', b('1')); + await crud.put([], 'baz', b('1')); + await crud.put([], 'qux', b('2')); + const list = await crud.list([]); + expect(list.length).toBe(3); + expect(list.find(x => x.id === 'baz')).toMatchObject({ + type: 'resource', + id: 'baz', + }); + expect(list.find(x => x.id === 'qux')).toMatchObject({ + type: 'resource', + id: 'qux', + }); + expect(list.find(x => x.id === 'foo')).toMatchObject({ + type: 'collection', + id: 'foo', + }); + }); + + test('throws when try to list a non-existing collection', async () => { + const { crud } = setup(); + await crud.put(['foo'], 'bar', b('1')); + await crud.put([], 'baz', b('1')); + await crud.put([], 'qux', b('2')); + const [, err] = await of(crud.list(['gg'])); + expect(err).toBeInstanceOf(DOMException); + expect((err).name).toBe('CollectionNotFound'); + }); + }); +}; diff --git a/src/fsa-to-crud/util.ts b/src/crud/util.ts similarity index 83% rename from src/fsa-to-crud/util.ts rename to src/crud/util.ts index a112c9360..a009bc345 100644 --- a/src/fsa-to-crud/util.ts +++ b/src/crud/util.ts @@ -1,4 +1,4 @@ -import { CrudCollection } from '../crud/types'; +import { CrudCollection } from './types'; import { assertName } from '../node-to-fsa/util'; export const assertType = (type: CrudCollection, method: string, klass: string): void => { diff --git a/src/fsa-to-crud/FsaCrud.ts b/src/fsa-to-crud/FsaCrud.ts index c45fc1bc9..352bcaa0f 100644 --- a/src/fsa-to-crud/FsaCrud.ts +++ b/src/fsa-to-crud/FsaCrud.ts @@ -1,7 +1,7 @@ import type * as crud from '../crud/types'; import type * as fsa from '../fsa/types'; import { assertName } from '../node-to-fsa/util'; -import { assertType } from './util'; +import { assertType } from '../crud/util'; export class FsaCrud implements crud.CrudApi { public constructor( diff --git a/src/fsa-to-crud/__tests__/FsaCrud.test.ts b/src/fsa-to-crud/__tests__/FsaCrud.test.ts index 2ee2038c5..bac306c04 100644 --- a/src/fsa-to-crud/__tests__/FsaCrud.test.ts +++ b/src/fsa-to-crud/__tests__/FsaCrud.test.ts @@ -1,8 +1,8 @@ -import { of } from 'thingies'; import { memfs } from '../..'; import { onlyOnNode20 } from '../../__tests__/util'; import { NodeFileSystemDirectoryHandle } from '../../node-to-fsa'; import { FsaCrud } from '../FsaCrud'; +import { testCrudfs } from '../../crud/__tests__/testCrudfs'; const setup = () => { const fs = memfs(); @@ -11,308 +11,6 @@ const setup = () => { return { fs, fsa, crud, snapshot: () => (fs).__vol.toJSON() }; }; -const b = (str: string) => { - const buf = Buffer.from(str); - return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); -}; - onlyOnNode20('FsaCrud', () => { - describe('.put()', () => { - test('throws if the type is not valid', async () => { - const { crud } = setup(); - const [, err] = await of(crud.put(['.', 'foo'], 'bar', new Uint8Array())); - expect(err).toBeInstanceOf(TypeError); - expect((err).message).toBe("Failed to execute 'put' on 'crudfs': Name is not allowed."); - }); - - test('throws if id is not valid', async () => { - const { crud } = setup(); - const [, err] = await of(crud.put(['foo'], '..', new Uint8Array())); - expect(err).toBeInstanceOf(TypeError); - expect((err).message).toBe("Failed to execute 'put' on 'crudfs': Name is not allowed."); - }); - - test('can store a resource at root', async () => { - const { crud, snapshot } = setup(); - await crud.put([], 'bar', b('abc')); - expect(snapshot()).toStrictEqual({ - '/bar': 'abc', - }); - }); - - test('can store a resource in two levels deep collection', async () => { - const { crud, snapshot } = setup(); - await crud.put(['a', 'b'], 'bar', b('abc')); - expect(snapshot()).toStrictEqual({ - '/a/b/bar': 'abc', - }); - }); - - test('can overwrite existing resource', async () => { - const { crud, snapshot } = setup(); - await crud.put(['a', 'b'], 'bar', b('abc')); - await crud.put(['a', 'b'], 'bar', b('efg')); - expect(snapshot()).toStrictEqual({ - '/a/b/bar': 'efg', - }); - }); - - test('can choose to throw if item already exists', async () => { - const { crud } = setup(); - await crud.put(['a', 'b'], 'bar', b('abc'), { throwIf: 'exists' }); - const [, err] = await of(crud.put(['a', 'b'], 'bar', b('efg'), { throwIf: 'exists' })); - expect(err).toBeInstanceOf(DOMException); - expect((err).name).toBe('Exists'); - }); - - test('can choose to throw if item does not exist', async () => { - const { crud, snapshot } = setup(); - const [, err] = await of(crud.put(['a', 'b'], 'bar', b('1'), { throwIf: 'missing' })); - await crud.put(['a', 'b'], 'bar', b('2')); - await crud.put(['a', 'b'], 'bar', b('3'), { throwIf: 'missing' }); - expect(err).toBeInstanceOf(DOMException); - expect((err).name).toBe('Missing'); - expect(snapshot()).toStrictEqual({ - '/a/b/bar': '3', - }); - }); - }); - - describe('.get()', () => { - test('throws if the type is not valid', async () => { - const { crud } = setup(); - const [, err] = await of(crud.get(['', 'foo'], 'bar')); - expect(err).toBeInstanceOf(TypeError); - expect((err).message).toBe("Failed to execute 'get' on 'crudfs': Name is not allowed."); - }); - - test('throws if id is not valid', async () => { - const { crud } = setup(); - const [, err] = await of(crud.get(['foo'], '')); - expect(err).toBeInstanceOf(TypeError); - expect((err).message).toBe("Failed to execute 'get' on 'crudfs': Name is not allowed."); - }); - - test('throws if collection does not exist', async () => { - const { crud } = setup(); - const [, err] = await of(crud.get(['foo'], 'bar')); - expect(err).toBeInstanceOf(DOMException); - expect((err).name).toBe('CollectionNotFound'); - }); - - test('throws if resource does not exist', async () => { - const { crud } = setup(); - await crud.put(['foo'], 'bar', b('abc')); - const [, err] = await of(crud.get(['foo'], 'baz')); - expect(err).toBeInstanceOf(DOMException); - expect((err).name).toBe('ResourceNotFound'); - }); - - test('can fetch an existing resource', async () => { - const { crud } = setup(); - await crud.put(['foo'], 'bar', b('abc')); - const blob = await crud.get(['foo'], 'bar'); - expect(blob).toStrictEqual(b('abc')); - }); - }); - - describe('.del()', () => { - test('throws if the type is not valid', async () => { - const { crud } = setup(); - const [, err] = await of(crud.del(['sdf\\dd', 'foo'], 'bar')); - expect(err).toBeInstanceOf(TypeError); - expect((err).message).toBe("Failed to execute 'del' on 'crudfs': Name is not allowed."); - }); - - test('throws if id is not valid', async () => { - const { crud } = setup(); - const [, err] = await of(crud.del(['foo'], 'asdf/asdf')); - expect(err).toBeInstanceOf(TypeError); - expect((err).message).toBe("Failed to execute 'del' on 'crudfs': Name is not allowed."); - }); - - describe('when collection does not exist', () => { - test('throws by default', async () => { - const { crud } = setup(); - const [, err] = await of(crud.del(['foo'], 'bar')); - expect(err).toBeInstanceOf(DOMException); - expect((err).name).toBe('CollectionNotFound'); - }); - - test('does not throw when "silent" flag set', async () => { - const { crud } = setup(); - await crud.del(['foo'], 'bar', true); - }); - }); - - describe('when collection is found but resource is not', () => { - test('throws by default', async () => { - const { crud } = setup(); - await crud.put(['foo'], 'bar', b('abc')); - const [, err] = await of(crud.del(['foo'], 'baz')); - expect(err).toBeInstanceOf(DOMException); - expect((err).name).toBe('ResourceNotFound'); - }); - - test('does not throw when "silent" flag set', async () => { - const { crud } = setup(); - await crud.put(['foo'], 'bar', b('abc')); - await crud.del(['foo'], 'baz', true); - }); - }); - - test('deletes an existing resource', async () => { - const { crud } = setup(); - await crud.put(['foo'], 'bar', b('abc')); - await crud.get(['foo'], 'bar'); - await crud.del(['foo'], 'bar'); - const [, err] = await of(crud.get(['foo'], 'bar')); - expect(err).toBeInstanceOf(DOMException); - expect((err).name).toBe('ResourceNotFound'); - }); - }); - - describe('.info()', () => { - test('throws if the type is not valid', async () => { - const { crud } = setup(); - const [, err] = await of(crud.info(['', 'foo'], 'bar')); - expect(err).toBeInstanceOf(TypeError); - expect((err).message).toBe("Failed to execute 'info' on 'crudfs': Name is not allowed."); - }); - - test('throws if id is not valid', async () => { - const { crud } = setup(); - const [, err] = await of(crud.info(['foo'], '/')); - expect(err).toBeInstanceOf(TypeError); - expect((err).message).toBe("Failed to execute 'info' on 'crudfs': Name is not allowed."); - }); - - test('can retrieve information about a resource', async () => { - const { crud } = setup(); - await crud.put(['foo'], 'bar', b('abc')); - const info = await crud.info(['foo'], 'bar'); - expect(info).toMatchObject({ - type: 'resource', - id: 'bar', - size: 3, - modified: expect.any(Number), - }); - }); - - test('can retrieve information about a collection', async () => { - const { crud } = setup(); - await crud.put(['foo'], 'bar', b('abc')); - const info = await crud.info(['foo']); - expect(info).toMatchObject({ - type: 'collection', - }); - }); - - test('throws when resource not found', async () => { - const { crud } = setup(); - await crud.put(['foo'], 'bar', b('abc')); - const [, err] = await of(crud.info(['foo'], 'baz')); - expect(err).toBeInstanceOf(DOMException); - expect((err).name).toBe('ResourceNotFound'); - }); - - test('throws when collection not found', async () => { - const { crud } = setup(); - await crud.put(['foo', 'a'], 'bar', b('abc')); - const [, err] = await of(crud.info(['foo', 'b'], 'baz')); - expect(err).toBeInstanceOf(DOMException); - expect((err).name).toBe('CollectionNotFound'); - }); - }); - - describe('.drop()', () => { - test('throws if the collection is not valid', async () => { - const { crud } = setup(); - const [, err] = await of(crud.drop(['', 'foo'])); - expect(err).toBeInstanceOf(TypeError); - expect((err).message).toBe("Failed to execute 'drop' on 'crudfs': Name is not allowed."); - }); - - test('can recursively delete a collection', async () => { - const { crud } = setup(); - await crud.put(['foo', 'a'], 'bar', b('1')); - await crud.put(['foo', 'a'], 'baz', b('2')); - await crud.put(['foo', 'b'], 'xyz', b('3')); - const info = await crud.info(['foo', 'a']); - expect(info.type).toBe('collection'); - await crud.drop(['foo', 'a']); - const [, err] = await of(crud.info(['foo', 'a'])); - expect(err).toBeInstanceOf(DOMException); - expect((err).name).toBe('CollectionNotFound'); - }); - - test('throws if collection does not exist', async () => { - const { crud } = setup(); - await crud.put(['foo', 'a'], 'bar', b('1')); - await crud.put(['foo', 'a'], 'baz', b('2')); - await crud.put(['foo', 'b'], 'xyz', b('3')); - const [, err] = await of(crud.drop(['gg'])); - expect(err).toBeInstanceOf(DOMException); - expect((err).name).toBe('CollectionNotFound'); - }); - - test('when "silent" flag set, does not throw if collection does not exist', async () => { - const { crud } = setup(); - await crud.put(['foo', 'a'], 'bar', b('1')); - await crud.put(['foo', 'a'], 'baz', b('2')); - await crud.put(['foo', 'b'], 'xyz', b('3')); - await crud.drop(['gg'], true); - }); - - test('can recursively delete everything from root', async () => { - const { crud, snapshot } = setup(); - await crud.put(['foo', 'a'], 'bar', b('1')); - await crud.put(['baz', 'a'], 'baz', b('2')); - await crud.put(['bar', 'b'], 'xyz', b('3')); - const info = await crud.info(['foo', 'a']); - expect(info.type).toBe('collection'); - await crud.drop([]); - expect(snapshot()).toEqual({}); - }); - }); - - describe('.list()', () => { - test('throws if the collection is not valid', async () => { - const { crud } = setup(); - const [, err] = await of(crud.list(['./..', 'foo'])); - expect(err).toBeInstanceOf(TypeError); - expect((err).message).toBe("Failed to execute 'drop' on 'crudfs': Name is not allowed."); - }); - - test('can retrieve a list of resources and collections at root', async () => { - const { crud } = setup(); - await crud.put(['foo'], 'bar', b('1')); - await crud.put([], 'baz', b('1')); - await crud.put([], 'qux', b('2')); - const list = await crud.list([]); - expect(list.length).toBe(3); - expect(list.find(x => x.id === 'baz')).toMatchObject({ - type: 'resource', - id: 'baz', - }); - expect(list.find(x => x.id === 'qux')).toMatchObject({ - type: 'resource', - id: 'qux', - }); - expect(list.find(x => x.id === 'foo')).toMatchObject({ - type: 'collection', - id: 'foo', - }); - }); - - test('throws when try to list a non-existing collection', async () => { - const { crud } = setup(); - await crud.put(['foo'], 'bar', b('1')); - await crud.put([], 'baz', b('1')); - await crud.put([], 'qux', b('2')); - const [, err] = await of(crud.list(['gg'])); - expect(err).toBeInstanceOf(DOMException); - expect((err).name).toBe('CollectionNotFound'); - }); - }); + testCrudfs(setup); }); diff --git a/src/node-to-crud/NodeCrud.ts b/src/node-to-crud/NodeCrud.ts new file mode 100644 index 000000000..f4bc6b4db --- /dev/null +++ b/src/node-to-crud/NodeCrud.ts @@ -0,0 +1,172 @@ +import { assertName } from '../node-to-fsa/util'; +import { assertType } from '../crud/util'; +import type { FsPromisesApi } from '../node/types'; +import type * as crud from '../crud/types'; + +export interface NodeCrudOptions { + fs: FsPromisesApi; + dir: string; + separator?: string; +} + +export class NodeCrud implements crud.CrudApi { + public constructor( + options: NodeCrudOptions, + ) {} + + // protected async getDir( + // collection: crud.CrudCollection, + // create: boolean, + // ): Promise<[dir: fsa.IFileSystemDirectoryHandle, parent: fsa.IFileSystemDirectoryHandle | undefined]> { + // let parent: undefined | fsa.IFileSystemDirectoryHandle = undefined; + // let dir = await this.root; + // try { + // for (const name of collection) { + // const child = await dir.getDirectoryHandle(name, { create }); + // parent = dir; + // dir = child; + // } + // return [dir, parent]; + // } catch (error) { + // if (error.name === 'NotFoundError') + // throw new DOMException(`Collection /${collection.join('/')} does not exist`, 'CollectionNotFound'); + // throw error; + // } + // } + + // protected async getFile( + // collection: crud.CrudCollection, + // id: string, + // ): Promise<[dir: fsa.IFileSystemDirectoryHandle, file: fsa.IFileSystemFileHandle]> { + // const [dir] = await this.getDir(collection, false); + // try { + // const file = await dir.getFileHandle(id, { create: false }); + // return [dir, file]; + // } catch (error) { + // if (error.name === 'NotFoundError') + // throw new DOMException(`Resource "${id}" in /${collection.join('/')} not found`, 'ResourceNotFound'); + // throw error; + // } + // } + + public readonly put = async ( + collection: crud.CrudCollection, + id: string, + data: Uint8Array, + options?: crud.CrudPutOptions, + ): Promise => { + assertType(collection, 'put', 'crudfs'); + assertName(id, 'put', 'crudfs'); + throw new Error('Not implemented'); + // const [dir] = await this.getDir(collection, true); + // let file: fsa.IFileSystemFileHandle | undefined; + // switch (options?.throwIf) { + // case 'exists': { + // try { + // file = await dir.getFileHandle(id, { create: false }); + // throw new DOMException('Resource already exists', 'Exists'); + // } catch (e) { + // if (e.name !== 'NotFoundError') throw e; + // file = await dir.getFileHandle(id, { create: true }); + // } + // break; + // } + // case 'missing': { + // try { + // file = await dir.getFileHandle(id, { create: false }); + // } catch (e) { + // if (e.name === 'NotFoundError') throw new DOMException('Resource is missing', 'Missing'); + // throw e; + // } + // break; + // } + // default: { + // file = await dir.getFileHandle(id, { create: true }); + // } + // } + // const writable = await file!.createWritable(); + // await writable.write(data); + // await writable.close(); + }; + + public readonly get = async (collection: crud.CrudCollection, id: string): Promise => { + assertType(collection, 'get', 'crudfs'); + assertName(id, 'get', 'crudfs'); + throw new Error('Not implemented'); + // const [, file] = await this.getFile(collection, id); + // const blob = await file.getFile(); + // const buffer = await blob.arrayBuffer(); + // return new Uint8Array(buffer); + }; + + public readonly del = async (collection: crud.CrudCollection, id: string, silent?: boolean): Promise => { + assertType(collection, 'del', 'crudfs'); + assertName(id, 'del', 'crudfs'); + throw new Error('Not implemented'); + // try { + // const [dir] = await this.getFile(collection, id); + // await dir.removeEntry(id, { recursive: false }); + // } catch (error) { + // if (!silent) throw error; + // } + }; + + public readonly info = async (collection: crud.CrudCollection, id?: string): Promise => { + assertType(collection, 'info', 'crudfs'); + throw new Error('Not implemented'); + // if (id) { + // assertName(id, 'info', 'crudfs'); + // const [, file] = await this.getFile(collection, id); + // const blob = await file.getFile(); + // return { + // type: 'resource', + // id, + // size: blob.size, + // modified: blob.lastModified, + // }; + // } else { + // await this.getDir(collection, false); + // return { + // type: 'collection', + // id: '', + // }; + // } + }; + + public readonly drop = async (collection: crud.CrudCollection, silent?: boolean): Promise => { + assertType(collection, 'drop', 'crudfs'); + throw new Error('Not implemented'); + // try { + // const [dir, parent] = await this.getDir(collection, false); + // if (parent) { + // await parent.removeEntry(dir.name, { recursive: true }); + // } else { + // const root = await this.root; + // for await (const name of root.keys()) await root.removeEntry(name, { recursive: true }); + // } + // } catch (error) { + // if (!silent) throw error; + // } + }; + + public readonly list = async (collection: crud.CrudCollection): Promise => { + assertType(collection, 'drop', 'crudfs'); + throw new Error('Not implemented'); + // const [dir] = await this.getDir(collection, false); + // const entries: crud.CrudCollectionEntry[] = []; + // for await (const [id, handle] of dir.entries()) { + // if (handle.kind === 'file') { + // entries.push({ + // type: 'resource', + // id, + // }); + // } else if (handle.kind === 'directory') { + // entries.push({ + // type: 'collection', + // id, + // }); + // } + // } + // return entries; + }; +} diff --git a/src/node-to-crud/__tests__/FsaCrud.test.ts b/src/node-to-crud/__tests__/FsaCrud.test.ts new file mode 100644 index 000000000..68895e251 --- /dev/null +++ b/src/node-to-crud/__tests__/FsaCrud.test.ts @@ -0,0 +1,17 @@ +import { memfs } from '../..'; +import { onlyOnNode20 } from '../../__tests__/util'; +import { NodeCrud } from '../NodeCrud'; +import {testCrudfs} from '../../crud/__tests__/testCrudfs'; + +const setup = () => { + const fs = memfs(); + const crud = new NodeCrud({ + fs: fs.promises, + dir: '/', + }); + return { fs, crud, snapshot: () => (fs).__vol.toJSON() }; +}; + +onlyOnNode20('NodeCrud', () => { + testCrudfs(setup); +});