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)
-
+![](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)
+
+
+
@@ -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"]),