Skip to content

Commit

Permalink
feat: 🎸 implement crudfs .put() method
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jun 20, 2023
1 parent 18c0658 commit 505dc20
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 18 deletions.
41 changes: 23 additions & 18 deletions src/crud/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,65 @@ export interface CrudApi {
/**
* Creates a new resource, or overwrites an existing one.
*
* @param type Type of the resource, collection name.
* @param collection Type of the resource, collection name.
* @param id Id of the resource, document name.
* @param data Blob content of the resource.
* @param options Write behavior options.
*/
put: (type: CrudType, id: string, data: Uint8Array, options: CrudPutOptions) => Promise<void>;
put: (collection: CrudType, id: string, data: Uint8Array, options: CrudPutOptions) => Promise<void>;

/**
* Retrieves the content of a resource.
*
* @param type Type of the resource, collection name.
* @param collection Type of the resource, collection name.
* @param id Id of the resource, document name.
* @returns Blob content of the resource.
*/
get: (type: CrudType, id: string) => Promise<Uint8Array>;
get: (collection: CrudType, id: string) => Promise<Uint8Array>;

/**
* Deletes a resource.
*
* @param type Type of the resource, collection name.
* @param collection Type of the resource, collection name.
* @param id Id of the resource, document name.
*/
del: (type: CrudType, id: string) => Promise<void>;
del: (collection: CrudType, id: string) => Promise<void>;

/**
* Fetches information about a resource.
*
* @param type Type of the resource, collection name.
* @param collection Type of the resource, collection name.
* @param id Id of the resource, document name, if any.
* @returns Information about the resource.
*/
info: (type: CrudType, id?: string) => Promise<CrudResourceInfo>;
info: (collection: CrudType, id?: string) => Promise<CrudResourceInfo>;

/**
* Deletes all resources of a type, and deletes recursively all sub-collections.
* Deletes all resources of a collection, and deletes recursively all sub-collections.
*
* @param type Type of the resource, collection name.
* @param collection Type of the resource, collection name.
*/
drop: (type: CrudType) => Promise<void>;
drop: (collection: CrudType) => Promise<void>;

/**
* Fetches a list of resources of a type, and sub-collections.
* Fetches a list of resources of a collection, and sub-collections.
*
* @param type Type of the resource, collection name.
* @returns List of resources of the given type, and sub-collections.
* @param collection Type of the resource, collection name.
* @returns List of resources of the given type, and sub-types.
*/
list: (type: CrudType) => Promise<CrudTypeEntry[]>;
list: (collection: CrudType) => Promise<CrudTypeEntry[]>;

/**
* Recursively scans all resources of a type, and sub-collections. Returns
* Recursively scans all resources of a collection, and sub-collections. Returns
* a cursor to continue scanning.
*
* @param type Type of the resource, collection name.
* @param collection Type of the resource, collection name.
* @param cursor Cursor to start scanning from. If empty string, starts from the beginning.
* @returns List of resources of the given type, and sub-collections. Also
* returns a cursor to continue scanning. If the cursor is empty
* string, the scan is complete.
*/
scan: (type: CrudType, cursor?: string | '') => Promise<CrudScanResult>;
scan: (collection: CrudType, cursor?: string | '') => Promise<CrudScanResult>;
}

export type CrudType = string[];
Expand Down Expand Up @@ -89,3 +89,8 @@ export interface CrudScanResult {
cursor: string | '';
list: CrudTypeEntry[];
}

export interface CrudScanEntry extends CrudTypeEntry {
/** Collection, which contains this entry. */
type: CrudType;
}
73 changes: 73 additions & 0 deletions src/fsa-crud/FsaCrud.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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';

export class FsaCrud implements crud.CrudApi {
public constructor (protected readonly root: fsa.IFileSystemDirectoryHandle | Promise<fsa.IFileSystemDirectoryHandle>) {}

protected async getDir(type: crud.CrudType): Promise<fsa.IFileSystemDirectoryHandle> {
let dir = await this.root;
for (const name of type)
dir = await dir.getDirectoryHandle(name, {create: true});
return dir;
}

public readonly put = async (type: crud.CrudType, id: string, data: Uint8Array, options?: crud.CrudPutOptions): Promise<void> => {
assertType(type, 'put', 'crudfs');
assertName(id, 'put', 'crudfs');
const dir = await this.getDir(type);
let file: fsa.IFileSystemFileHandle | undefined;
switch (options?.throwIf) {
case 'exists': {
try {
file = await dir.getFileHandle(id, {create: false});
throw new DOMException('Resource already exists', 'ExistsError');
} 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', 'MissingError');
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 (type: crud.CrudType, id: string): Promise<Uint8Array> => {
throw new Error('Not implemented');
};

public readonly del = async (type: crud.CrudType, id: string): Promise<void> => {
throw new Error('Not implemented');
};

public readonly info = async (type: crud.CrudType, id?: string): Promise<crud.CrudResourceInfo> => {
throw new Error('Not implemented');
};

public readonly drop = async (type: crud.CrudType): Promise<void> => {
throw new Error('Not implemented');
};

public readonly list = async (type: crud.CrudType): Promise<crud.CrudTypeEntry[]> => {
throw new Error('Not implemented');
};

public readonly scan = async (type: crud.CrudType, cursor?: string | ''): Promise<crud.CrudScanResult> => {
throw new Error('Not implemented');
};
}
75 changes: 75 additions & 0 deletions src/fsa-crud/__tests__/FsaCrud.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {of} from 'thingies';
import {memfs} from '../..';
import {onlyOnNode20} from '../../__tests__/util';
import {NodeFileSystemDirectoryHandle} from '../../node-to-fsa';
import {FsaCrud} from '../FsaCrud';

const setup = () => {
const fs = memfs();
const fsa = new NodeFileSystemDirectoryHandle(fs, '/', {mode: 'readwrite'});
const crud = new FsaCrud(fsa);
return {fs, fsa, crud, snapshot: () => (<any>fs).__vol.toJSON()};
};

const b = (str: string) => Buffer.from(str).subarray(0);

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);
});

test('throws if id is not valid', async () => {
const {crud} = setup();
const [, err] = await of(crud.put(['foo'], '', new Uint8Array()));
expect(err).toBeInstanceOf(TypeError);
});

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((<DOMException>err).name).toBe('ExistsError');
});

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((<DOMException>err).name).toBe('MissingError');
expect(snapshot()).toStrictEqual({
'/a/b/bar': '3',
});
});
});
});
7 changes: 7 additions & 0 deletions src/fsa-crud/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {CrudType} from "../crud/types";
import {assertName} from "../node-to-fsa/util";

export const assertType = (type: CrudType, method: string, klass: string): void => {
const length = type.length;
for (let i = 0; i < length; i++) assertName(type[i], method, klass);
};

0 comments on commit 505dc20

Please sign in to comment.