diff --git a/README.md b/README.md index c285543..4f725df 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ ![](https://raw.githubusercontent.com/Tarasikee/tinydb/v1.0.0-alpha/images/Logo1.png) -
- CI status - CODEql analysis - - Deno link - -
+![](https://github.com/Tarasikee/tinydb/actions/workflows/ci.yml/badge.svg) +![](https://github.com/Tarasikee/tinydb/actions/workflows/codeql-analysis.yml/badge.svg) +![](https://img.shields.io/github/license/Tarasikee/tinydb) +![](https://img.shields.io/github/v/release/Tarasikee/tinydb) + +Deno link +
@@ -18,8 +18,12 @@ Tiny, Powerful, Beautiful - [Motivation](#motivation) - [Let's start](#lets-start) -- [@TinyTable](#lets-start) -- [@Column](#lets-start) +- [CRUD](#crud) + - [Create](#create) + - [Retrieve](#retrieve) + - [Update](#update) + - [Delete (Hunters)](#delete-hunters) +- [Contributing](#contributing) # Motivation @@ -33,62 +37,219 @@ store and retrieve data. It has all the features of a relational database, but it designed to be as lightweight and simple as possible. -No need to install software or to set up a server. You're ready to go after -installing dependencies. +No need to install software or to set up a server. Just import the library, and +you are ready to go. # Let's start -Your entry point is ```@TinyTable``` decorator, where you pass table's name. +Your entry point is ```@TinyTable``` decorator, +where you pass table's name and path to the +file where you want to store your data. -No need to remember ton of decorators. Simply start with ```@Column({})```, add a -small bunch of properties, -and you are ready to go. -In the example below you will see the best way to use create user with TinyDB. +Below you can see an example of how to use ```@TinyTable``` and ```@Column``` decorators. -```typescript -@TinyTable("users") +```ts +@TinyTable({ + name: "users", + url: "database/example1/" +}) class User { @Column({ type: "string", - unique: true, + unique: true }) name!: string - @Column({ - type: "string", - allowNull: false, - }) - password!: string - - @Column({ - type: "date", - allowNull: true, - }) - birthday!: string - @Column({ type: "boolean", - default: false, allowNull: true, + default: false, }) isAdmin!: boolean @Column({ - allowNull: true, type: "json", - default: { - theme: 'light', - lang: 'en', - } - }) - settings!: Record - - @Column({ - type: "array", allowNull: true, - default: [], + default: { + theme: "dark", + lang: "en" + }, }) - friends!: string[] + settings!: { + theme: "dark" | "light" + lang: "en" | "ua" + } } + +export type userDocument = User & Document + +const userSchema = Schema.initializeSchema(User) +export const userModel = new Model(userSchema) +// if you want to short: +// export const userModel = new Model(Schema.initializeSchema(User)) +``` + +As you can see, there area bunch of options you can pass to ```@Column``` decorator: + +1. `unique`: Type is `boolean`. It will check on each new document if there is already a document with the same value +2. `type`: Type is `"string" | "number" | "boolean" | "date" | "json" | "array"`. + It will define type of column and will check it +3. `allowNull`: Type is `boolean`. If it is `true`, then column can be empty +4. `default`: Type is `any`. If column is empty, then it will be filled with default value + +#### Note: + +You should remember that there is no need to create `default` value if `allowNull` set to false. + +After class is created, you should create type of document and initialize +schema with ```Schema.initializeSchema``` function. +Then you can create model with ```new Model``` function. + +# CRUD + +CRUD is provided by `Model` class itself. Create and retrieve methods return type of `Instance` class, +that can be converted to JSON with .toJSON() method. + +### Create: + +To create a new document, you should call `create` method of `Model` class and pass object with data. +TinyDB will check if all required fields are filled and if all fields are valid. Then it will either +throw an error or create a new document and return it. + +#### Note: + +By default, TinyDB will generate unique id for each document, but if you want to handle it yourself, +you can do it by passing `_id` field to `create` method. Remember: it has to be an unique string. + +```ts +const user = userModel.create({ + //_id: "1", custom id + name: "Admin", + isAdmin: true +}) +user.save() +``` + +### Retrieve: + +Retrieve methods, as said before, return `Instance` class. Below you can see all of them: + +1. `find`: Returns all instances that match query + +```ts +const users = userModel.find({ + settings: { + theme: "dark", + } +}) ``` +2. `findOne`: Returns first instance that matches query + +```ts +const user = userModel.findOne({ + settings: { + lang: "ua", + } +}) +``` + +3. `findById`: Returns instance with specified id + +```ts +const userById = userModel.findById("1") +``` + +4. `findAll`: Returns all instances of model + +```ts +const allUsers = userModel.findAll() +``` + +#### Note: + +Both `find` and `findOne` methods can search by deeply nested objects. Here is +an [example]("https://github.com/Tarasikee/tinydb/blob/master/tests/findingTests.ts#L41"). +But be careful because it can affect performance. + +### Update: + +Update methods work the exact same way as retrieve methods, but +provide second argument that is update. + +1. `update`: Updates all instances that match query + +```ts +const users = userModel.findAndUpdate({ + settings: { + theme: "dark", + } +}, { + settings: { + theme: "light", + } +}) +``` + +2. `findOneAndUpdate`: Updates first instance that matches query + +```ts +const user = userModel.findOneAndUpdate({ + settings: { + lang: "ua", + } +}, { + settings: { + lang: "en", + } +}) +``` + +3. `findByIdAndUpdate`: Updates instance with specified id + +```ts +const userById = userModel.findByIdAndUpdate("1", { + name: "John", +}) +``` + +### Delete (Hunters): + +Delete methods work also the same way as retrieve methods, but delete +instance from table and return string. + +1. `hunt`: Deletes all instances that match query + +```ts +userModel.hunt({ + settings: { + theme: "dark", + } +}) +``` + +2. `huntOne`: Deletes first instance that matches query + +```ts +userModel.huntOne({ + settings: { + lang: "ua", + } +}) +``` + +3. `huntById`: Deletes instance with specified id + +```ts +userModel.huntById("1") +``` + +4. `huntAll`: Deletes all instances of model + +```ts +userModel.huntAll() +``` + +# Contributing + +If you want to contribute to this project, you can do it by creating pull request or by creating issue. diff --git a/database/example1/users.json b/database/example1/users.json index e69de29..67ae920 100644 --- a/database/example1/users.json +++ b/database/example1/users.json @@ -0,0 +1 @@ +[{"name":"Test5","_id":"ec51f74d-e065-4add-9cdd-e2311d565506","isAdmin":false}] \ No newline at end of file diff --git a/deps/deps.ts b/deps/deps.ts index 87cf6d6..93b6c58 100644 --- a/deps/deps.ts +++ b/deps/deps.ts @@ -1,4 +1,3 @@ -export {parse, format} from "https://deno.land/std@0.144.0/datetime/mod.ts" export {ensureDirSync} from "https://deno.land/std@0.78.0/fs/mod.ts" export {faker} from "https://deno.land/x/deno_faker@v1.0.3/mod.ts" export { diff --git a/example/user_with_profile_abc_ex/mod.ts b/example/user_with_profile_abc_ex/mod.ts index 2cf6175..db3d625 100644 --- a/example/user_with_profile_abc_ex/mod.ts +++ b/example/user_with_profile_abc_ex/mod.ts @@ -1,7 +1,7 @@ import {Application, Context, HttpException} from "https://deno.land/x/abc@v1.3.3/mod.ts" import {logger} from "https://deno.land/x/abc@v1.3.3/middleware/logger.ts" -import {userDocument, userModel} from "./models/user.model.ts" import {Status} from "https://deno.land/std@0.152.0/http/http_status.ts" +import {userDocument, userModel} from "./models/user.model.ts" import {ErrorHandler} from "../../src/errors/ErrorHandler.ts" const app = new Application() @@ -37,6 +37,13 @@ const findOne = (ctx: Context) => { ctx.json(user) } +const update = async (ctx: Context) => { + const body = await ctx.body + const {id} = ctx.params + const user = userModel.findByIdAndUpdate(id, body as userDocument) + ctx.json(user) +} + const deleteOne = (ctx: Context) => { const {id} = ctx.params const message = userModel.huntById(id) @@ -46,6 +53,7 @@ const deleteOne = (ctx: Context) => { app .get("/users", findAll) .get("/users/:id", findOne) + .put("/users/:id", update) .post("/users", create) .delete("/users/:id", deleteOne) .start({port: 8080}) diff --git a/src/classes/Instance.ts b/src/classes/Instance.ts index 07b2edd..cf60bce 100644 --- a/src/classes/Instance.ts +++ b/src/classes/Instance.ts @@ -1,4 +1,4 @@ -import {ColumnsUtils, FileUtils, Schema} from "../mod.ts" +import {ColumnsChecks, FileUtils, Schema} from "../mod.ts" interface InstanceOptions { isNew: boolean @@ -45,16 +45,14 @@ export class Instance { const table = this.getTable() if (this._options.isNew) { - const _id = this._fields._id || crypto.randomUUID() - - new ColumnsUtils(this._schema.columns, table, this._fields) - table.push({...this._fields, _id}) - this._fields._id = _id + this._fields._id = this._fields._id || crypto.randomUUID() + new ColumnsChecks(this._schema.columns, table, this._fields) + table.push(this._fields) this.writeTable([...table]) } else { const filteredTable = table.filter(row => row._id !== this._fields._id) - new ColumnsUtils(this._schema.columns, filteredTable, this._fields) + new ColumnsChecks(this._schema.columns, filteredTable, this._fields) filteredTable.push(this._fields) this.writeTable([...filteredTable]) diff --git a/src/classes/Model.ts b/src/classes/Model.ts index f9055f3..1f37ce5 100644 --- a/src/classes/Model.ts +++ b/src/classes/Model.ts @@ -49,6 +49,29 @@ export class Model { .map(row => new Instance(this.schema, row, {isNew: false})) } + //Updaters + public findByIdAndUpdate(_id: string, args: Partial) { + const instance = this.findById(_id) + instance.fields = {...instance.fields, ...args} + instance.save() + return instance + } + + public findAndUpdate(args: Partial, update: Partial) { + this.find(args).map(instance => { + instance.fields = {...instance.fields, ...update} + instance.save() + }) + return "Successful update!" + } + + public findOneAndUpdate(args: Partial, update: Partial) { + const instance = this.findOne(args) + instance.fields = {...instance.fields, ...update} + instance.save() + return instance + } + // Hunters public huntById(_id: string) { this.findById(_id).delete() diff --git a/src/classes/Schema.ts b/src/classes/Schema.ts index fbc5d24..67e7d6a 100644 --- a/src/classes/Schema.ts +++ b/src/classes/Schema.ts @@ -27,6 +27,8 @@ export class Schema { .filter(key => key[0] !== "_") .map(key => ({name: key, options: getFormat(instance, key)})) - return new Schema(instance["_tableName"], instance["_dir_url"], options) + return new Schema(instance["_tableName"], instance["_dir_url"], [...options, { + name: "_id", options: {type: "string", unique: true} + }]) } } diff --git a/src/decorators/Column.ts b/src/decorators/Column.ts index 189de31..7e4d9ac 100644 --- a/src/decorators/Column.ts +++ b/src/decorators/Column.ts @@ -8,9 +8,8 @@ export function getFormat(target: unknown, propertyKey: string) { } export function Column(options: ColumnProps) { - const optionsProxy = options.allowNull === undefined + return Reflect.metadata(formatMetadataKey, options.allowNull === undefined ? {...options, allowNull: false} : {...options, allowNull: true} - - return Reflect.metadata(formatMetadataKey, optionsProxy) + ) } diff --git a/src/mod.ts b/src/mod.ts index 5149a11..4ab0b9c 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -2,7 +2,7 @@ export {Model, Instance, Schema, Table} from "./classes/mod.ts" // Utils -export {FileUtils, ObjectUtils, ColumnsUtils} from "./utils/mod.ts" +export {FileUtils, ObjectUtils, ColumnsChecks} from "./utils/mod.ts" // Decorators export {Column, getFormat, TinyTable} from "./decorators/mod.ts" diff --git a/src/utils/ColumnsUtils.ts b/src/utils/ColumnsChecks.ts similarity index 94% rename from src/utils/ColumnsUtils.ts rename to src/utils/ColumnsChecks.ts index b237cf2..e52674d 100644 --- a/src/utils/ColumnsUtils.ts +++ b/src/utils/ColumnsChecks.ts @@ -1,8 +1,7 @@ import {ColumnRules, OptionTypes} from "../interfaces/mod.ts" import {ErrorWithHint, ErrorWithMessage} from "../errors/mod.ts" -import {parse} from "../../deps/deps.ts" -export class ColumnsUtils { +export class ColumnsChecks { constructor( private columnRules: ColumnRules[] = [], private table: Array = [], @@ -16,7 +15,7 @@ export class ColumnsUtils { if (checkType === "date") { try { - return parse(String(value), "yyyy-MM-dd") + return new Date(String(value)).toISOString() } catch (_) { throw new ErrorWithMessage(`${columnName} must be date`) } diff --git a/src/utils/mod.ts b/src/utils/mod.ts index 61ff528..39f0dd6 100644 --- a/src/utils/mod.ts +++ b/src/utils/mod.ts @@ -1,3 +1,3 @@ -export {ColumnsUtils} from './ColumnsUtils.ts' +export {ColumnsChecks} from './ColumnsChecks.ts' export {FileUtils} from './FileUtils.ts' export {ObjectUtils} from './ObjectUtils.ts' diff --git a/tests/creationTests.ts b/tests/creationTests.ts index fa66a1a..71caf8d 100644 --- a/tests/creationTests.ts +++ b/tests/creationTests.ts @@ -1,12 +1,12 @@ import {userDocument, userModel} from "./userInitiale.ts" -import {faker, format, assertStrictEquals, assertThrows, assertObjectMatch} from "../deps/deps.ts" +import {faker, assertStrictEquals, assertThrows, assertObjectMatch} from "../deps/deps.ts" export const creationTests = () => { return Deno.test("Creation tests", async (t) => { await t.step("Create user with all fields", () => { const user = userModel.create({ name: faker.name.firstName(), - birthday: format(faker.date.past(), "yyyy-MM-dd"), + birthday: new Date(faker.date.past()).toISOString(), isAdmin: faker.random.boolean(), settings: { theme: faker.random.arrayElement(["light", "dark", "system"]), @@ -23,7 +23,7 @@ export const creationTests = () => { () => { const users = [...Array(800).keys()].map(() => userModel.create({ name: faker.name.firstName(), - birthday: format(faker.date.past(), "yyyy-MM-dd"), + birthday: new Date(faker.date.past()).toISOString(), isAdmin: faker.random.boolean(), settings: { theme: faker.random.arrayElement(["light", "dark", "system"]), @@ -48,7 +48,7 @@ export const creationTests = () => { () => { const user = userModel.create({ name: 123, - birthday: format(faker.date.past(), "yyyy-MM-dd"), + birthday: new Date(faker.date.past()).toISOString(), isAdmin: faker.random.boolean(), settings: { theme: faker.random.arrayElement(["light", "dark", "system"]), @@ -86,7 +86,7 @@ export const creationTests = () => { () => { const user = userModel.create({ name: faker.name.firstName(), - birthday: format(faker.date.past(), "yyyy-MM-dd"), + birthday: new Date(faker.date.past()).toISOString(), isAdmin: 123, settings: { theme: faker.random.arrayElement(["light", "dark", "system"]), @@ -105,7 +105,7 @@ export const creationTests = () => { () => { const user = userModel.create({ name: faker.name.firstName(), - birthday: format(faker.date.past(), "yyyy-MM-dd"), + birthday: new Date(faker.date.past()).toISOString(), isAdmin: faker.random.boolean(), settings: [] } as unknown as userDocument) @@ -166,7 +166,7 @@ export const creationTests = () => { { _id: "123", name: "John", - birthday: "2000-01-29", + birthday: new Date("2000-01-29").toISOString(), isAdmin: true, settings: { theme: "darcula", diff --git a/tests/findingTests.ts b/tests/findingTests.ts index 96ec582..af9611d 100644 --- a/tests/findingTests.ts +++ b/tests/findingTests.ts @@ -1,12 +1,12 @@ import {userModel} from "./userInitiale.ts" -import {faker, assertStrictEquals, format} from "../deps/deps.ts" +import {faker, assertStrictEquals} from "../deps/deps.ts" export const findingTests = () => { return Deno.test("Finding tests", async (t) => { await t.step("Create 10 users for testing purposes", () => { const users = [...Array(10).keys()].map(() => userModel.create({ name: faker.name.firstName(), - birthday: format(faker.date.past(), "yyyy-MM-dd"), + birthday: new Date(faker.date.past()).toISOString(), isAdmin: faker.random.boolean(), settings: { theme: faker.random.arrayElement(["light", "dark", "system"]),