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
5 changes: 2 additions & 3 deletions apps/example/src/fs-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type MySchema = {
price: number;
};
};

const adapter = new FileSystemAdapter("./data");
const db = createClient<MySchema>(adapter);

Expand All @@ -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();
4 changes: 1 addition & 3 deletions apps/example/src/s3-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
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;
});
```
112 changes: 13 additions & 99 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,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;
Expand All @@ -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 {
Expand All @@ -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) {
Expand All @@ -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<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(
"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<Model>
) {
const data = await this.getById(collection, id);
const modified = await fn(data);
return await this.update(collection, modified);
}
}
93 changes: 5 additions & 88 deletions packages/clyve/src/adapters/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -75,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 @@ -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<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 @@ -154,37 +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!))
);
}

async edit(
collection: string,
id: string,
fn: (entity: Model) => Model | Promise<Model>
) {
const data = await this.getById(collection, id);
const modified = await fn(data);
return await this.update(collection, modified);
}
}
14 changes: 1 addition & 13 deletions packages/clyve/src/adapters/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +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>;
edit: (
collection: string,
id: string,
fn: (entity: Model) => Model | Promise<Model>
) => Promise<Model>;
}
30 changes: 17 additions & 13 deletions packages/clyve/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, Model>;

type ClyveClient<T extends Schema> = {
export type ClyveClient<T extends Schema> = {
[K in keyof T]: {
get: (id: string) => Promise<T[K]>;
all: () => Promise<Array<T[K]>>;
Expand All @@ -25,26 +26,29 @@ type ClyveClient<T extends Schema> = {
};

export function createClient<T extends Schema>(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<string>) => 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<Model>) =>
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<string>) =>
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<Model>) =>
adapter.edit(collection, id, fn),
operations.edit(collection, id, fn),
};
},
set() {
Expand Down
Loading
Loading