Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 5 additions & 14 deletions apps/example/src/fs-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { S3Client } from "@aws-sdk/client-s3";
import { createClient } from "clyve";
import { FileSystemAdapter } from "clyve/adapters";
import "dotenv/config";
Expand All @@ -15,20 +14,12 @@ type MySchema = {
};
};

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<MySchema>(adapter);

await db.users.deleteAll();

const hej = await db.users.all();
await db.users.upsert({
id: "1",
name: "Wall-e",
});

console.log(hej);
await db.users.deleteAll();
4 changes: 0 additions & 4 deletions apps/example/src/s3-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,4 @@ await db.users.upsert({
name: "Wall-e",
});

const exists = await db.users.exists("1");

console.log(exists);

await db.users.deleteAll();
8 changes: 8 additions & 0 deletions packages/clyve/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
```
2 changes: 1 addition & 1 deletion packages/clyve/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
116 changes: 25 additions & 91 deletions packages/clyve/src/adapters/file-system.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -17,15 +17,25 @@ 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);
const string = await fs.readFile(filePath, "utf-8");

return JSON.parse(string);
}
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;
}

async getById(collection: string, id: string) {
return await this.getByKey(`${collection}/${id}.json`);
return JSON.parse(content);
}

async exists(collection: string, id: string) {
Expand All @@ -38,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) {
Expand All @@ -76,71 +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<Model>) {
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<string>) {
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}`)
)
);
}
}
1 change: 1 addition & 0 deletions packages/clyve/src/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./s3.js";
export * from "./file-system.js";
export * from "./types.js";
106 changes: 22 additions & 84 deletions packages/clyve/src/adapters/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ import {
GetObjectCommand,
HeadObjectCommand,
ListObjectsV2Command,
NoSuchKey,
NotFound,
PutObjectCommand,
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];
Expand All @@ -28,12 +25,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");
Expand Down Expand Up @@ -64,25 +71,17 @@ 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,
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!)));
return files
.filter((file) => file.Key !== undefined)
.map((file) => file.Key!);
}

async upsert(collection: string, data: Model) {
Expand All @@ -97,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<Model>) {
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({
Expand All @@ -143,27 +104,4 @@ export class S3Adapter implements Adapter {
})
);
}

async deleteMany(collection: string, ids: Array<string>) {
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!))
);
}
}
9 changes: 1 addition & 8 deletions packages/clyve/src/adapters/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,8 @@ import { Model } from "model.js";

export interface Adapter {
getByKey: (key: string) => Promise<Model>;
getById: (collection: string, id: string) => Promise<Model>;
exists: (collection: string, id: string) => Promise<boolean>;
count: (collection: string) => Promise<number>;
all: (collection: string) => Promise<Model[]>;
keys: (collection: string) => Promise<string[]>;
upsert: (collection: string, data: Model) => Promise<Model>;
create: (collection: string, data: Model) => Promise<Model>;
update: (collection: string, data: Model) => Promise<Model>;
createMany: (collection: string, data: Model[]) => Promise<Model[]>;
deleteObject: (collection: string, id: string) => Promise<void>;
deleteMany: (collection: string, ids: string[]) => Promise<void>;
deleteAll: (collection: string) => Promise<void>;
}
Loading
Loading