diff --git a/apps/example/src/fs-adapter.ts b/apps/example/src/fs-adapter.ts index 09b18f1..7f0fef5 100644 --- a/apps/example/src/fs-adapter.ts +++ b/apps/example/src/fs-adapter.ts @@ -13,6 +13,7 @@ type MySchema = { price: number; }; }; + const adapter = new FileSystemAdapter("./data"); const db = createClient(adapter); @@ -21,6 +22,4 @@ await db.users.upsert({ name: "Wall-e", }); -const user = await db.users.get("1"); - -console.log(user); +await db.users.deleteAll(); diff --git a/apps/example/src/s3-adapter.ts b/apps/example/src/s3-adapter.ts index e75ca8a..08fb6e1 100644 --- a/apps/example/src/s3-adapter.ts +++ b/apps/example/src/s3-adapter.ts @@ -32,6 +32,4 @@ await db.users.upsert({ name: "Wall-e", }); -const user = await db.users.get("1"); - -console.log(user); +await db.users.deleteAll(); diff --git a/packages/clyve/README.md b/packages/clyve/README.md index 5365002..0bef7e1 100644 --- a/packages/clyve/README.md +++ b/packages/clyve/README.md @@ -155,3 +155,11 @@ Delete all entries in a collection: ```typescript await db.users.deleteAll(); ``` + +Edit an entry, a shortcut for performing a sequential `.get()` and `.update()` operation. +```typescript +await db.users.edit("1", (user) => { + user.name = "Wall-e 2"; + return user; +}); +``` \ No newline at end of file diff --git a/packages/clyve/src/adapters/file-system.ts b/packages/clyve/src/adapters/file-system.ts index 2dd5608..d0e8030 100644 --- a/packages/clyve/src/adapters/file-system.ts +++ b/packages/clyve/src/adapters/file-system.ts @@ -1,5 +1,5 @@ import { Model } from "../model.js"; -import { DuplicateKeyError, KeyDoesNotExistError } from "../errors.js"; +import { KeyDoesNotExistError } from "../errors.js"; import { Adapter } from "./types.js"; import { promises as fs } from "node:fs"; import path from "node:path"; @@ -17,6 +17,10 @@ export class FileSystemAdapter implements Adapter { await fs.writeFile(filePath, data); } + private isEnoentError(error: unknown): error is Error { + return error instanceof Error && "code" in error && error.code === "ENOENT"; + } + async getByKey(key: string) { const filePath = path.resolve(this.basePath, key); let content; @@ -34,10 +38,6 @@ export class FileSystemAdapter implements Adapter { return JSON.parse(content); } - async getById(collection: string, id: string) { - return await this.getByKey(`${collection}/${id}.json`); - } - async exists(collection: string, id: string) { const filePath = path.resolve(this.basePath, `${collection}/${id}.json`); try { @@ -48,33 +48,20 @@ export class FileSystemAdapter implements Adapter { } } - private isEnoentError(error: unknown): error is Error { - return error instanceof Error && "code" in error && error.code === "ENOENT"; - } - - private async listEntries(collection: string) { + async keys(collection: string) { + const directoryPath = path.resolve(this.basePath, collection); + let files; try { - const directoryPath = path.resolve(this.basePath, collection); - const files = await fs.readdir(directoryPath, { withFileTypes: true }); - return files.filter((file) => !file.isDirectory()); + files = await fs.readdir(directoryPath, { withFileTypes: true }); } catch (error) { if (this.isEnoentError(error)) { return []; } throw error; } - } - - async count(collection: string) { - const files = await this.listEntries(collection); - return files.length; - } - - async all(collection: string) { - const files = await this.listEntries(collection); - return await Promise.all( - files.map((file) => this.getByKey(`${collection}/${file.name}`)) - ); + return files + .filter((file) => !file.isDirectory()) + .map((file) => path.posix.join(collection, file.name)); } async upsert(collection: string, data: Model) { @@ -86,81 +73,8 @@ export class FileSystemAdapter implements Adapter { return data; } - async create(collection: string, data: Model) { - const doesKeyAlreadyExist = await this.exists(collection, data.id); - if (doesKeyAlreadyExist) { - throw new DuplicateKeyError( - `Key ${collection}/${data.id}.json already exists` - ); - } - - return await this.upsert(collection, data); - } - - async update(collection: string, data: Model) { - const doesKeyAlreadyExist = await this.exists(collection, data.id); - if (!doesKeyAlreadyExist) { - throw new KeyDoesNotExistError( - `Key ${collection}/${data.id}.json does not exist` - ); - } - - return await this.upsert(collection, data); - } - - async createMany(collection: string, data: Array) { - const entriesExist = await Promise.all( - data.map((data) => this.exists(collection, data.id)) - ); - - const someExist = entriesExist.some((exists) => exists); - if (someExist) { - throw new DuplicateKeyError( - "Cannot create items because some key already exist" - ); - } - - return await Promise.all(data.map((data) => this.upsert(collection, data))); - } - async deleteObject(collection: string, id: string) { - const filePath = path.resolve(this.basePath, `${collection}/${id}.json`); + const filePath = path.resolve(this.basePath, collection, `${id}.json`); await fs.unlink(filePath); } - - async deleteMany(collection: string, ids: Array) { - const entriesExist = await Promise.all( - ids.map((id) => this.exists(collection, id)) - ); - - const allExist = entriesExist.every((exists) => exists); - - if (!allExist) { - throw new KeyDoesNotExistError( - "Could not delete items because some key does not exist" - ); - } - - await Promise.all(ids.map((id) => this.deleteObject(collection, id))); - } - - async deleteAll(collection: string) { - const files = await this.listEntries(collection); - - await Promise.all( - files.map((file) => - this.deleteObject(collection, `${collection}/${file.name}`) - ) - ); - } - - async edit( - collection: string, - id: string, - fn: (entity: Model) => Model | Promise - ) { - const data = await this.getById(collection, id); - const modified = await fn(data); - return await this.update(collection, modified); - } } diff --git a/packages/clyve/src/adapters/s3.ts b/packages/clyve/src/adapters/s3.ts index 75c828f..23eec63 100644 --- a/packages/clyve/src/adapters/s3.ts +++ b/packages/clyve/src/adapters/s3.ts @@ -9,11 +9,7 @@ import { S3Client, } from "@aws-sdk/client-s3"; import { Adapter } from "./types.js"; -import { - DuplicateKeyError, - KeyDoesNotExistError, - NoBodyError, -} from "../errors.js"; +import { KeyDoesNotExistError, NoBodyError } from "../errors.js"; import { Model } from "../model.js"; export type S3AdapterConstructorParams = [s3Client: S3Client, bucket: string]; @@ -75,7 +71,7 @@ export class S3Adapter implements Adapter { } } - private async listEntries(collection: string) { + async keys(collection: string) { const response = await this.client.send( new ListObjectsV2Command({ Bucket: this.bucket, @@ -83,17 +79,9 @@ export class S3Adapter implements Adapter { }) ); const files = response.Contents ?? []; - return files; - } - - async count(collection: string) { - const files = await this.listEntries(collection); - return files.length; - } - - async all(collection: string) { - const files = await this.listEntries(collection); - return await Promise.all(files.map((file) => this.getByKey(file.Key!))); + return files + .filter((file) => file.Key !== undefined) + .map((file) => file.Key!); } async upsert(collection: string, data: Model) { @@ -108,44 +96,6 @@ export class S3Adapter implements Adapter { return data; } - async create(collection: string, data: Model) { - const doesKeyAlreadyExist = await this.exists(collection, data.id); - if (doesKeyAlreadyExist) { - throw new DuplicateKeyError( - `Key ${collection}/${data.id}.json already exists` - ); - } - - return await this.upsert(collection, data); - } - - async update(collection: string, data: Model) { - const doesKeyAlreadyExist = await this.exists(collection, data.id); - if (!doesKeyAlreadyExist) { - throw new KeyDoesNotExistError( - `Key ${collection}/${data.id}.json does not exist` - ); - } - - return await this.upsert(collection, data); - } - - async createMany(collection: string, data: Array) { - const entriesExist = await Promise.all( - data.map((data) => this.exists(collection, data.id)) - ); - - const someExist = entriesExist.some((exists) => exists); - - if (someExist) { - throw new DuplicateKeyError( - "Cannot create items because some key already exist" - ); - } - - return await Promise.all(data.map((data) => this.upsert(collection, data))); - } - async deleteObject(collection: string, id: string) { await this.client.send( new DeleteObjectCommand({ @@ -154,37 +104,4 @@ export class S3Adapter implements Adapter { }) ); } - - async deleteMany(collection: string, ids: Array) { - const entriesExist = await Promise.all( - ids.map((id) => this.exists(collection, id)) - ); - - const allExist = entriesExist.every((exists) => exists); - - if (!allExist) { - throw new KeyDoesNotExistError( - "Cannot delete items because some key does not exist" - ); - } - - await Promise.all(ids.map((id) => this.deleteObject(collection, id))); - } - - async deleteAll(collection: string) { - const files = await this.listEntries(collection); - await Promise.all( - files.map((file) => this.deleteObject(collection, file.Key!)) - ); - } - - async edit( - collection: string, - id: string, - fn: (entity: Model) => Model | Promise - ) { - const data = await this.getById(collection, id); - const modified = await fn(data); - return await this.update(collection, modified); - } } diff --git a/packages/clyve/src/adapters/types.ts b/packages/clyve/src/adapters/types.ts index 6838b1a..8698937 100644 --- a/packages/clyve/src/adapters/types.ts +++ b/packages/clyve/src/adapters/types.ts @@ -2,20 +2,8 @@ import { Model } from "model.js"; export interface Adapter { getByKey: (key: string) => Promise; - getById: (collection: string, id: string) => Promise; exists: (collection: string, id: string) => Promise; - count: (collection: string) => Promise; - all: (collection: string) => Promise; + keys: (collection: string) => Promise; upsert: (collection: string, data: Model) => Promise; - create: (collection: string, data: Model) => Promise; - update: (collection: string, data: Model) => Promise; - createMany: (collection: string, data: Model[]) => Promise; deleteObject: (collection: string, id: string) => Promise; - deleteMany: (collection: string, ids: string[]) => Promise; - deleteAll: (collection: string) => Promise; - edit: ( - collection: string, - id: string, - fn: (entity: Model) => Model | Promise - ) => Promise; } diff --git a/packages/clyve/src/index.ts b/packages/clyve/src/index.ts index 104b2eb..e2340cc 100644 --- a/packages/clyve/src/index.ts +++ b/packages/clyve/src/index.ts @@ -1,10 +1,11 @@ import { CollectionObjectReadOnlyError } from "./errors.js"; import { Adapter } from "./adapters/types.js"; import { Model } from "./model.js"; +import { Operations } from "./operations.js"; type Schema = Record; -type ClyveClient = { +export type ClyveClient = { [K in keyof T]: { get: (id: string) => Promise; all: () => Promise>; @@ -25,26 +26,29 @@ type ClyveClient = { }; export function createClient(adapter: Adapter) { + const operations = new Operations(adapter); + return new Proxy( {}, { get(_, key) { const collection = key.toString(); return { - get: (id: string) => adapter.getById(collection, id), - all: () => adapter.all(collection), - create: (data: Model) => adapter.create(collection, data), - delete: (id: string) => adapter.deleteObject(collection, id), - deleteMany: (id: Array) => adapter.deleteMany(collection, id), - deleteAll: () => adapter.deleteAll(collection), + get: (id: string) => operations.get(collection, id), + all: () => operations.all(collection), + create: (data: Model) => operations.create(collection, data), createMany: (data: Array) => - adapter.createMany(collection, data), - count: () => adapter.count(collection), - exists: (id: string) => adapter.exists(collection, id), - update: (data: Model) => adapter.update(collection, data), - upsert: (data: Model) => adapter.upsert(collection, data), + operations.createMany(collection, data), + delete: (id: string) => operations.deleteObject(collection, id), + deleteMany: (id: Array) => + operations.deleteMany(collection, id), + deleteAll: () => operations.deleteAll(collection), + count: () => operations.count(collection), + exists: (id: string) => operations.exists(collection, id), + update: (data: Model) => operations.update(collection, data), + upsert: (data: Model) => operations.upsert(collection, data), edit: (id: string, fn: (entity: Model) => Model | Promise) => - adapter.edit(collection, id, fn), + operations.edit(collection, id, fn), }; }, set() { diff --git a/packages/clyve/src/operations.ts b/packages/clyve/src/operations.ts new file mode 100644 index 0000000..ee5c44b --- /dev/null +++ b/packages/clyve/src/operations.ts @@ -0,0 +1,121 @@ +import { Adapter } from "./adapters/types.js"; +import { DuplicateKeyError, KeyDoesNotExistError } from "./errors.js"; +import { Model } from "./model.js"; + +export class Operations { + private adapter: Adapter; + + constructor(adapter: Adapter) { + this.adapter = adapter; + } + + async getById(collection: string, id: string) { + return await this.adapter.getByKey(`${collection}/${id}.json`); + } + + async get(collection: string, id: string) { + return await this.getById(collection, id); + } + + async all(collection: string) { + const keys = await this.adapter.keys(collection); + return await Promise.all(keys.map((key) => this.adapter.getByKey(key))); + } + + async create(collection: string, data: Model) { + const doesKeyAlreadyExist = await this.adapter.exists(collection, data.id); + if (doesKeyAlreadyExist) { + throw new DuplicateKeyError( + `Key ${collection}/${data.id}.json already exists` + ); + } + + return await this.adapter.upsert(collection, data); + } + + async createMany(collection: string, data: Array) { + const entriesExist = await Promise.all( + data.map((data) => this.adapter.exists(collection, data.id)) + ); + const someExist = entriesExist.some((exists) => exists); + if (someExist) { + throw new DuplicateKeyError( + "Cannot create items because some key already exist" + ); + } + + return await Promise.all( + data.map((data) => this.adapter.upsert(collection, data)) + ); + } + + async deleteObject(collection: string, id: string) { + await this.adapter.deleteObject(collection, id); + } + + async deleteMany(collection: string, ids: Array) { + const entriesExist = await Promise.all( + ids.map((id) => this.adapter.exists(collection, id)) + ); + + const allExist = entriesExist.every((exists) => exists); + + if (!allExist) { + throw new KeyDoesNotExistError( + "Cannot delete items because some key does not exist" + ); + } + + await Promise.all( + ids.map((id) => this.adapter.deleteObject(collection, id)) + ); + } + + async allIds(collection: string) { + const keys = await this.adapter.keys(collection); + const fileNames = keys.map((key) => key.split("/")[1]); + const ids = fileNames.map((fileName) => fileName.split(".")[0]); + return ids; + } + + async deleteAll(collection: string) { + const ids = await this.allIds(collection); + await Promise.all( + ids.map((id) => this.adapter.deleteObject(collection, id)) + ); + } + + async count(collection: string) { + const keys = await this.adapter.keys(collection); + return keys.length; + } + + async exists(collection: string, id: string) { + return await this.adapter.exists(collection, id); + } + + async update(collection: string, data: Model) { + const doesKeyAlreadyExist = await this.exists(collection, data.id); + if (!doesKeyAlreadyExist) { + throw new KeyDoesNotExistError( + `Key ${collection}/${data.id}.json does not exist` + ); + } + + return await this.upsert(collection, data); + } + + async upsert(collection: string, data: Model) { + return await this.adapter.upsert(collection, data); + } + + async edit( + collection: string, + id: string, + fn: (entity: Model) => Model | Promise + ) { + const data = await this.getById(collection, id); + const modified = await fn(data); + return await this.update(collection, modified); + } +}