From 0009f84406a5caf64df937ce8a5711a13fca240e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Ekl=C3=B6f?= <60542867+feelixe@users.noreply.github.com> Date: Sat, 14 Dec 2024 22:29:05 +0100 Subject: [PATCH 1/9] feat/adapter (#1) * feat: add adapters * docs: update readme with adapter creation * feat: add filesystem adapter * feat: add example * feat: add fs adapter example * ci: add build workflow --- .github/workflows/build.yml | 32 ++++ apps/example/package.json | 3 +- apps/example/src/fs-adapter.ts | 34 +++++ apps/example/src/index.ts | 93 ------------ apps/example/src/s3-adapter.ts | 39 +++++ package.json | 2 +- packages/clyve/README.md | 38 ++++- packages/clyve/package.json | 4 + packages/clyve/src/adapters/file-system.ts | 146 ++++++++++++++++++ packages/clyve/src/adapters/index.ts | 2 + packages/clyve/src/adapters/s3.ts | 169 +++++++++++++++++++++ packages/clyve/src/adapters/types.ts | 16 ++ packages/clyve/src/errors.ts | 8 +- packages/clyve/src/model.ts | 5 + pnpm-lock.yaml | 4 +- 15 files changed, 489 insertions(+), 106 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 apps/example/src/fs-adapter.ts delete mode 100644 apps/example/src/index.ts create mode 100644 apps/example/src/s3-adapter.ts create mode 100644 packages/clyve/src/adapters/file-system.ts create mode 100644 packages/clyve/src/adapters/index.ts create mode 100644 packages/clyve/src/adapters/s3.ts create mode 100644 packages/clyve/src/adapters/types.ts create mode 100644 packages/clyve/src/model.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..a7a777f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,32 @@ +name: Build + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - "**" + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: pnpm install + + - name: Build + run: pnpm run build diff --git a/apps/example/package.json b/apps/example/package.json index 19d7a74..feab65b 100644 --- a/apps/example/package.json +++ b/apps/example/package.json @@ -4,7 +4,8 @@ "private": true, "main": "index.js", "scripts": { - "exec": "vite-node ./src/index.ts" + "s3": "vite-node ./src/s3-adapter.ts", + "fs": "vite-node ./src/fs-adapter.ts" }, "keywords": [], "author": "", diff --git a/apps/example/src/fs-adapter.ts b/apps/example/src/fs-adapter.ts new file mode 100644 index 0000000..ea932f8 --- /dev/null +++ b/apps/example/src/fs-adapter.ts @@ -0,0 +1,34 @@ +import { S3Client } from "@aws-sdk/client-s3"; +import { createClient } from "clyve"; +import { FileSystemAdapter } from "clyve/adapters"; +import "dotenv/config"; + +type MySchema = { + users: { + id: string; + name: string; + }; + products: { + id: string; + name: string; + price: number; + }; +}; + +export const s3Client = new S3Client({ + endpoint: process.env.S3_ENDPOINT!, + region: process.env.S3_REGION!, + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY!, + secretAccessKey: process.env.S3_SECRET_KEY!, + }, +}); + +const adapter = new FileSystemAdapter("./data"); +const db = createClient(adapter); + +await db.users.deleteAll(); + +const hej = await db.users.all(); + +console.log(hej); diff --git a/apps/example/src/index.ts b/apps/example/src/index.ts deleted file mode 100644 index ceda3aa..0000000 --- a/apps/example/src/index.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { S3Client } from "@aws-sdk/client-s3"; -import { createClient } from "clyve"; -import { DuplicateKeyError } from "clyve/errors"; -import "dotenv/config"; - -new DuplicateKeyError(); - -type MySchema = { - users: { - id: string; - name: string; - }; - products: { - id: string; - name: string; - price: number; - }; -}; - -export const s3Client = new S3Client({ - endpoint: process.env.S3_ENDPOINT!, - region: process.env.S3_REGION!, - credentials: { - accessKeyId: process.env.S3_ACCESS_KEY!, - secretAccessKey: process.env.S3_SECRET_KEY!, - }, -}); - -const db = createClient(s3Client, "scoreboard-app"); - -await db.users.exists("1"); - -const exists = await db.users.exists("1"); - -console.log(exists); - -await db.users.deleteAll(); - -await db.users.deleteMany(["1", "2"]); - -// db.users.create({ -// "id": "1", -// "name": "Truls" -// }) - -// await db.users.create({ -// id: "1", -// name: "Truls", -// }); - -// await db.users.createMany([ -// { -// id: "1", -// name: "Truls", -// }, -// { -// id: "2", -// name: "Jan-Ove Waldner", -// } -// ]); - -// const all = await db.products.all(); -// console.log(all); - -// const ps3 = await db.products.get("ps3"); -// console.log(ps3); - -// await db.products.createMany([ -// { -// id: "ps5", -// name: "Playstation 5", -// price: 499, -// }, -// { -// id: "xbox", -// name: "Xbox Series X", -// price: 499, -// }, -// ]); - -// const all2 = await db.products.all(); -// console.log(all2); - -// await db.products.delete("ps5"); - -// const numProds = await db.products.count(); -// console.log(numProds); - -// const jan = await db.users.get("1"); - -// const users = await db.users.all(); - -// await db.users.delete("1"); diff --git a/apps/example/src/s3-adapter.ts b/apps/example/src/s3-adapter.ts new file mode 100644 index 0000000..48b1a40 --- /dev/null +++ b/apps/example/src/s3-adapter.ts @@ -0,0 +1,39 @@ +import { S3Client } from "@aws-sdk/client-s3"; +import { createClient } from "clyve"; +import { S3Adapter } from "clyve/adapters"; +import "dotenv/config"; + +type MySchema = { + users: { + id: string; + name: string; + }; + products: { + id: string; + name: string; + price: number; + }; +}; + +export const s3Client = new S3Client({ + endpoint: process.env.S3_ENDPOINT!, + region: process.env.S3_REGION!, + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY!, + secretAccessKey: process.env.S3_SECRET_KEY!, + }, +}); + +const adapter = new S3Adapter(s3Client, "scoreboard-app"); +const db = createClient(adapter); + +await db.users.upsert({ + id: "1", + name: "Wall-e", +}); + +const exists = await db.users.exists("1"); + +console.log(exists); + +await db.users.deleteAll(); diff --git a/package.json b/package.json index 6e88b54..0131758 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "clyve-workspace", "private": true, "scripts": { - "build": "pnpm run -r build", + "build": "pnpm run --parallel --stream -r build", "dev": "pnpm run --parallel --stream -r dev" }, "keywords": [], diff --git a/packages/clyve/README.md b/packages/clyve/README.md index 973a101..5263125 100644 --- a/packages/clyve/README.md +++ b/packages/clyve/README.md @@ -1,5 +1,5 @@ # Clyve -A lightweight client for using AWS S3 as a database. Perfect for quick MVPs or prototypes, it lets you store, retrieve, and manage JSON objects without a full database setup. While not suited for production, it takes advantage of S3’s affordability, durability, and scalability, enabling simple CRUD operations on structured data. +A lightweight client for using either AWS S3 or the filesystem as a database via adapters. Perfect for quick MVPs or prototypes, it lets you store, retrieve, and manage JSON objects without a full database setup. While not suited for production, it takes advantage of S3’s scalability or the simplicity of the filesystem, enabling easy CRUD operations on structured data. ## Key Features - 🕒 Quick to prototype and iterate with. @@ -7,7 +7,7 @@ A lightweight client for using AWS S3 as a database. Perfect for quick MVPs or p - 💸 Low cost. - 👨‍💻 Simple and developer friendly client. - 📠 No code generation or build step. -- 📦 No third-party dependencies (relies on AWS SDK as a peer dependency). +- 📦 No third-party dependencies. - 🔐 Fully type-safe with strong TypeScript support. ## Notes @@ -16,10 +16,15 @@ A lightweight client for using AWS S3 as a database. Perfect for quick MVPs or p ## Installation Install required packages ```bash -npm install clyve @aws-sdk/client-s3 +npm install clyve ``` -## Usage +Install the S3 client if you want to use the S3 adapter +```bash +npm install @aws-sdk/client-s3 +``` + +## Usage with S3 Adapter ```typescript import { S3Client } from "@aws-sdk/client-s3"; import { createClient } from "clyve"; @@ -47,10 +52,33 @@ type MySchema = { }; }; +// Create Clyve client. const bucketName = "my-bucket"; +const adapter = new S3Adapter(s3Client, bucketName); +const db = createClient(adapter); +``` + +## Usage with file system adapter +```typescript +import { createClient } from "clyve"; +import { FileSystemAdapter } from "clyve/adapters"; + +// Create your schema type, id is required in every model. +type MySchema = { + users: { + id: string; + name: string; + }; + products: { + id: string; + name: string; + price: number; + }; +}; // Create Clyve client. -const db = createClient(s3Client, bucketName); +const adapter = new FileSystemAdapter("./data"); +const db = createClient(adapter); ``` ## Operations diff --git a/packages/clyve/package.json b/packages/clyve/package.json index ea730d5..1779fad 100644 --- a/packages/clyve/package.json +++ b/packages/clyve/package.json @@ -18,6 +18,10 @@ "./errors": { "import": "./dist/errors.js", "types": "./dist/errors.d.ts" + }, + "./adapters": { + "import": "./dist/adapters/index.js", + "types": "./dist/adapters/index.d.ts" } }, "files": [ diff --git a/packages/clyve/src/adapters/file-system.ts b/packages/clyve/src/adapters/file-system.ts new file mode 100644 index 0000000..4729db7 --- /dev/null +++ b/packages/clyve/src/adapters/file-system.ts @@ -0,0 +1,146 @@ +import { Model } from "../model.js"; +import { DuplicateKeyError, KeyDoesNotExistError } from "../errors.js"; +import { Adapter } from "./types.js"; +import { promises as fs } from "node:fs"; +import path from "node:path"; + +export class FileSystemAdapter implements Adapter { + private basePath: string; + + constructor(basePath: string) { + this.basePath = basePath; + } + + private async writeFileWithDirs(filePath: string, data: string) { + const dir = path.dirname(filePath); + await fs.mkdir(dir, { recursive: true }); + await fs.writeFile(filePath, data); + } + + async getByKey(key: string) { + const filePath = path.resolve(this.basePath, key); + const string = await fs.readFile(filePath, "utf-8"); + + return JSON.parse(string); + } + + 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 { + await fs.access(filePath); + return true; + } catch { + return false; + } + } + + private isEnoentError(error: unknown): error is Error { + return error instanceof Error && "code" in error && error.code === "ENOENT"; + } + + private async listEntries(collection: string) { + try { + const directoryPath = path.resolve(this.basePath, collection); + const files = await fs.readdir(directoryPath, { withFileTypes: true }); + return files.filter((file) => !file.isDirectory()); + } 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}`)) + ); + } + + async upsert(collection: string, data: Model) { + const filePath = path.resolve( + this.basePath, + `${collection}/${data.id}.json` + ); + await this.writeFileWithDirs(filePath, JSON.stringify(data)); + 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`); + 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( + "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, `${collection}/${file.name}`) + ) + ); + } +} diff --git a/packages/clyve/src/adapters/index.ts b/packages/clyve/src/adapters/index.ts new file mode 100644 index 0000000..435e13e --- /dev/null +++ b/packages/clyve/src/adapters/index.ts @@ -0,0 +1,2 @@ +export * from "./s3.js"; +export * from "./file-system.js"; diff --git a/packages/clyve/src/adapters/s3.ts b/packages/clyve/src/adapters/s3.ts new file mode 100644 index 0000000..b5fda0f --- /dev/null +++ b/packages/clyve/src/adapters/s3.ts @@ -0,0 +1,169 @@ +import { + DeleteObjectCommand, + GetObjectCommand, + HeadObjectCommand, + ListObjectsV2Command, + NotFound, + PutObjectCommand, + S3Client, +} from "@aws-sdk/client-s3"; +import { Adapter } from "./types.js"; +import { + DuplicateKeyError, + KeyDoesNotExistError, + NoBodyError, +} from "../errors.js"; +import { Model } from "../model.js"; + +export type S3AdapterConstructorParams = [s3Client: S3Client, bucket: string]; + +export class S3Adapter implements Adapter { + private client: S3Client; + private bucket: string; + + constructor(...args: S3AdapterConstructorParams) { + const [s3Client, bucket] = args; + this.client = s3Client; + this.bucket = bucket; + } + + async getByKey(key: string) { + const response = await this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: key, + }) + ); + + if (!response.Body) { + throw new NoBodyError("S3 response did not have a body"); + } + + const string = await response.Body.transformToString(); + return JSON.parse(string); + } + + async getById(collection: string, id: string) { + return await this.getByKey(`${collection}/${id}.json`); + } + + async exists(collection: string, id: string) { + try { + await this.client.send( + new HeadObjectCommand({ + Bucket: this.bucket, + Key: `${collection}/${id}.json`, + }) + ); + return true; + } catch (error) { + if (error instanceof NotFound) { + return false; + } + throw error; + } + } + + private async listEntries(collection: string) { + const response = await this.client.send( + new ListObjectsV2Command({ + Bucket: this.bucket, + Prefix: `${collection}/`, + }) + ); + 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!))); + } + + async upsert(collection: string, data: Model) { + await this.client.send( + new PutObjectCommand({ + Bucket: this.bucket, + Key: `${collection}/${data.id}.json`, + Body: JSON.stringify(data), + ContentType: "application/json", + }) + ); + 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({ + Bucket: this.bucket, + Key: `${collection}/${id}.json`, + }) + ); + } + + 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!)) + ); + } +} diff --git a/packages/clyve/src/adapters/types.ts b/packages/clyve/src/adapters/types.ts new file mode 100644 index 0000000..107c8ba --- /dev/null +++ b/packages/clyve/src/adapters/types.ts @@ -0,0 +1,16 @@ +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; + 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; +} diff --git a/packages/clyve/src/errors.ts b/packages/clyve/src/errors.ts index 89aaa72..9b4a385 100644 --- a/packages/clyve/src/errors.ts +++ b/packages/clyve/src/errors.ts @@ -1,4 +1,4 @@ -export class DuplicateKeyError extends Error { } -export class CollectionObjectReadOnlyError extends Error { } -export class NoBodyError extends Error { } -export class KeyDoesNotExistError extends Error { } \ No newline at end of file +export class DuplicateKeyError extends Error {} +export class CollectionObjectReadOnlyError extends Error {} +export class NoBodyError extends Error {} +export class KeyDoesNotExistError extends Error {} diff --git a/packages/clyve/src/model.ts b/packages/clyve/src/model.ts new file mode 100644 index 0000000..c6f6888 --- /dev/null +++ b/packages/clyve/src/model.ts @@ -0,0 +1,5 @@ +import { JSONObject } from "json.js"; + +export type Model = JSONObject & { + id: string; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 689e8a1..64ee7fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,7 @@ importers: version: 3.709.0 clyve: specifier: workspace:* - version: link:../../packages/clyde + version: link:../../packages/clyve dotenv: specifier: ^16.4.7 version: 16.4.7 @@ -43,7 +43,7 @@ importers: specifier: ^2.1.8 version: 2.1.8(@types/node@22.10.2) - packages/clyde: + packages/clyve: devDependencies: '@aws-sdk/client-s3': specifier: ^3.709.0 From c793bb8690f1a6d54ab57eb8546f59d6313e31ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Ekl=C3=B6f?= Date: Sat, 14 Dec 2024 22:33:53 +0100 Subject: [PATCH 2/9] docs: add missing import in readme --- packages/clyve/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/clyve/README.md b/packages/clyve/README.md index 5263125..5365002 100644 --- a/packages/clyve/README.md +++ b/packages/clyve/README.md @@ -28,6 +28,7 @@ npm install @aws-sdk/client-s3 ```typescript import { S3Client } from "@aws-sdk/client-s3"; import { createClient } from "clyve"; +import { S3Adapter } from "clyve/adapters"; // Create an S3 client. export const s3Client = new S3Client({ From 8036a7aed1d501fd3068f3f8e1ca11881a6be424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Ekl=C3=B6f?= Date: Sat, 14 Dec 2024 22:35:50 +0100 Subject: [PATCH 3/9] chore: prepare 1.0.0 --- packages/clyve/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clyve/package.json b/packages/clyve/package.json index 1779fad..c381a95 100644 --- a/packages/clyve/package.json +++ b/packages/clyve/package.json @@ -1,7 +1,7 @@ { "name": "clyve", "type": "module", - "version": "0.1.1", + "version": "1.0.0", "description": "A lightweight TypeScript client that uses AWS S3 as a schema-driven, JSON-based database.", "homepage": "https://github.com/feelixe/clyve", "repository": { From 643ca47af387c2eb45da97e3db2f47c5929ce832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Ekl=C3=B6f?= Date: Sat, 14 Dec 2024 22:36:58 +0100 Subject: [PATCH 4/9] ci: rename job --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a7a777f..8ff93e7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ on: - "**" jobs: - test: + build: runs-on: ubuntu-latest steps: From 42b4487bdd44d605f0f2f14a12b5895067bb73f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Ekl=C3=B6f?= Date: Sat, 14 Dec 2024 22:55:55 +0100 Subject: [PATCH 5/9] chore: add s3 peer dep --- packages/clyve/package.json | 10 +++++++++- pnpm-lock.yaml | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/clyve/package.json b/packages/clyve/package.json index c381a95..1539346 100644 --- a/packages/clyve/package.json +++ b/packages/clyve/package.json @@ -38,9 +38,17 @@ "author": "", "license": "ISC", "devDependencies": { - "@aws-sdk/client-s3": "^3.709.0", + "@aws-sdk/client-s3": "^3.352.0", "@types/node": "^22.10.2", "typescript": "^5.7.2", "vite-node": "^2.1.8" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.352.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-s3": { + "optional": true + } } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64ee7fc..eafa0b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,7 +46,7 @@ importers: packages/clyve: devDependencies: '@aws-sdk/client-s3': - specifier: ^3.709.0 + specifier: ^3.352.0 version: 3.709.0 '@types/node': specifier: ^22.10.2 From 1c0f23c8d45daa3bdc5414110dfb39105f9a24ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Ekl=C3=B6f?= Date: Sat, 14 Dec 2024 22:59:40 +0100 Subject: [PATCH 6/9] chore: update package description --- packages/clyve/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clyve/package.json b/packages/clyve/package.json index 1539346..a8343f1 100644 --- a/packages/clyve/package.json +++ b/packages/clyve/package.json @@ -2,7 +2,7 @@ "name": "clyve", "type": "module", "version": "1.0.0", - "description": "A lightweight TypeScript client that uses AWS S3 as a schema-driven, JSON-based database.", + "description": "A lightweight TypeScript client that uses AWS S3 or the file system as a schema-driven, JSON-based database.", "homepage": "https://github.com/feelixe/clyve", "repository": { "type": "git", From 3c85dc87e07e6e9bc313b9d92b0c2a0cad12bdc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Ekl=C3=B6f?= <60542867+feelixe@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:21:11 +0100 Subject: [PATCH 7/9] feat: add edit entity function (#5) --- apps/example/src/fs-adapter.ts | 20 ++++--------- apps/example/src/s3-adapter.ts | 6 ++-- packages/clyve/src/adapters/file-system.ts | 26 +++++++++++++++-- packages/clyve/src/adapters/index.ts | 1 + packages/clyve/src/adapters/s3.ts | 33 +++++++++++++++++---- packages/clyve/src/adapters/types.ts | 5 ++++ packages/clyve/src/index.ts | 34 ++++++++++++---------- 7 files changed, 83 insertions(+), 42 deletions(-) diff --git a/apps/example/src/fs-adapter.ts b/apps/example/src/fs-adapter.ts index ea932f8..09b18f1 100644 --- a/apps/example/src/fs-adapter.ts +++ b/apps/example/src/fs-adapter.ts @@ -1,4 +1,3 @@ -import { S3Client } from "@aws-sdk/client-s3"; import { createClient } from "clyve"; import { FileSystemAdapter } from "clyve/adapters"; import "dotenv/config"; @@ -14,21 +13,14 @@ type MySchema = { price: number; }; }; - -export const s3Client = new S3Client({ - endpoint: process.env.S3_ENDPOINT!, - region: process.env.S3_REGION!, - credentials: { - accessKeyId: process.env.S3_ACCESS_KEY!, - secretAccessKey: process.env.S3_SECRET_KEY!, - }, -}); - const adapter = new FileSystemAdapter("./data"); const db = createClient(adapter); -await db.users.deleteAll(); +await db.users.upsert({ + id: "1", + name: "Wall-e", +}); -const hej = await db.users.all(); +const user = await db.users.get("1"); -console.log(hej); +console.log(user); diff --git a/apps/example/src/s3-adapter.ts b/apps/example/src/s3-adapter.ts index 48b1a40..e75ca8a 100644 --- a/apps/example/src/s3-adapter.ts +++ b/apps/example/src/s3-adapter.ts @@ -32,8 +32,6 @@ await db.users.upsert({ name: "Wall-e", }); -const exists = await db.users.exists("1"); +const user = await db.users.get("1"); -console.log(exists); - -await db.users.deleteAll(); +console.log(user); diff --git a/packages/clyve/src/adapters/file-system.ts b/packages/clyve/src/adapters/file-system.ts index 4729db7..2dd5608 100644 --- a/packages/clyve/src/adapters/file-system.ts +++ b/packages/clyve/src/adapters/file-system.ts @@ -19,9 +19,19 @@ export class FileSystemAdapter implements Adapter { async getByKey(key: string) { const filePath = path.resolve(this.basePath, key); - const string = await fs.readFile(filePath, "utf-8"); + let content; + try { + content = await fs.readFile(filePath, "utf-8"); + } catch (error) { + if (this.isEnoentError(error)) { + throw new KeyDoesNotExistError(`Key ${key} does not exist`, { + cause: error, + }); + } + throw error; + } - return JSON.parse(string); + return JSON.parse(content); } async getById(collection: string, id: string) { @@ -127,7 +137,7 @@ export class FileSystemAdapter implements Adapter { if (!allExist) { throw new KeyDoesNotExistError( - "Cannot delete items because some key does not exist" + "Could not delete items because some key does not exist" ); } @@ -143,4 +153,14 @@ export class FileSystemAdapter implements Adapter { ) ); } + + 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/index.ts b/packages/clyve/src/adapters/index.ts index 435e13e..24b3fb3 100644 --- a/packages/clyve/src/adapters/index.ts +++ b/packages/clyve/src/adapters/index.ts @@ -1,2 +1,3 @@ export * from "./s3.js"; export * from "./file-system.js"; +export * from "./types.js"; diff --git a/packages/clyve/src/adapters/s3.ts b/packages/clyve/src/adapters/s3.ts index b5fda0f..75c828f 100644 --- a/packages/clyve/src/adapters/s3.ts +++ b/packages/clyve/src/adapters/s3.ts @@ -3,6 +3,7 @@ import { GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, + NoSuchKey, NotFound, PutObjectCommand, S3Client, @@ -28,12 +29,22 @@ export class S3Adapter implements Adapter { } async getByKey(key: string) { - const response = await this.client.send( - new GetObjectCommand({ - Bucket: this.bucket, - Key: key, - }) - ); + let response; + try { + response = await this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: key, + }) + ); + } catch (error) { + if (error instanceof NoSuchKey) { + throw new KeyDoesNotExistError(`Key ${key} does not exist`, { + cause: error, + }); + } + throw error; + } if (!response.Body) { throw new NoBodyError("S3 response did not have a body"); @@ -166,4 +177,14 @@ export class S3Adapter implements Adapter { 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 107c8ba..6838b1a 100644 --- a/packages/clyve/src/adapters/types.ts +++ b/packages/clyve/src/adapters/types.ts @@ -13,4 +13,9 @@ export interface Adapter { 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 58212ca..104b2eb 100644 --- a/packages/clyve/src/index.ts +++ b/packages/clyve/src/index.ts @@ -17,30 +17,34 @@ type ClyveClient = { exists: (id: string) => Promise; update: (data: T[K]) => Promise; upsert: (data: T[K]) => Promise; + edit: ( + id: string, + fn: (entity: T[K]) => T[K] | Promise + ) => Promise; }; }; -export function createClient( - adapter: Adapter -): ClyveClient { +export function createClient(adapter: Adapter) { return new Proxy( {}, { get(_, key) { + const collection = key.toString(); return { - get: (id: string) => adapter.getById(key.toString(), id), - all: () => adapter.all(key.toString()), - create: (data: Model) => adapter.create(key.toString(), data), - delete: (id: string) => adapter.deleteObject(key.toString(), id), - deleteMany: (id: Array) => - adapter.deleteMany(key.toString(), id), - deleteAll: () => adapter.deleteAll(key.toString()), + 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), createMany: (data: Array) => - adapter.createMany(key.toString(), data), - count: () => adapter.count(key.toString()), - exists: (id: string) => adapter.exists(key.toString(), id), - update: (data: Model) => adapter.update(key.toString(), data), - upsert: (data: Model) => adapter.upsert(key.toString(), data), + 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), + edit: (id: string, fn: (entity: Model) => Model | Promise) => + adapter.edit(collection, id, fn), }; }, set() { From 6c2697d342fcca6f113767f40e2c871cf97f1ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Ekl=C3=B6f?= <60542867+feelixe@users.noreply.github.com> Date: Sun, 15 Dec 2024 20:24:04 +0100 Subject: [PATCH 8/9] feat: remove methods from adapter and move to operations (#6) --- apps/example/src/fs-adapter.ts | 5 +- apps/example/src/s3-adapter.ts | 4 +- packages/clyve/README.md | 8 ++ packages/clyve/src/adapters/file-system.ts | 112 +++---------------- packages/clyve/src/adapters/s3.ts | 93 +--------------- packages/clyve/src/adapters/types.ts | 14 +-- packages/clyve/src/index.ts | 30 ++--- packages/clyve/src/operations.ts | 121 +++++++++++++++++++++ 8 files changed, 168 insertions(+), 219 deletions(-) create mode 100644 packages/clyve/src/operations.ts 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); + } +} From 271a2fe900204a5b5bc0b13c6996a38aeefd8779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Ekl=C3=B6f?= Date: Sun, 15 Dec 2024 20:26:09 +0100 Subject: [PATCH 9/9] chore: prepare 1.1.1 --- packages/clyve/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clyve/package.json b/packages/clyve/package.json index 582177f..46ad90e 100644 --- a/packages/clyve/package.json +++ b/packages/clyve/package.json @@ -1,7 +1,7 @@ { "name": "clyve", "type": "module", - "version": "1.0.1", + "version": "1.1.1", "description": "A lightweight TypeScript client that uses AWS S3 or the file system as a schema-driven, JSON-based database.", "homepage": "https://github.com/feelixe/clyve", "repository": {