diff --git a/.gitignore b/.gitignore index 763d4cc..424cb89 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store .env .idea +database diff --git a/import_map.json b/import_map.json new file mode 100644 index 0000000..760b3b6 --- /dev/null +++ b/import_map.json @@ -0,0 +1,9 @@ +{ + "imports": { + "@core": "./src/mod.ts", + "@core/classes": "./src/classes/mod.ts", + "@core/decorators": "./src/decorators/mod.ts", + "@core/utils": "./src/utils/mod.ts", + "@core/errors": "./src/errors/mod.ts" + } +} diff --git a/import_maps.json b/import_maps.json deleted file mode 100644 index 3278a4f..0000000 --- a/import_maps.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "imports": { - "foo": "" - } -} diff --git a/src/Instance.ts b/src/Instance.ts new file mode 100644 index 0000000..b2fbcef --- /dev/null +++ b/src/Instance.ts @@ -0,0 +1,58 @@ +import {Schema} from "./mod.ts" +import {FileUtils, ColumnsUtils} from "./utils/mod.ts" + +interface InstanceOptions { + isNew: boolean; +} + +export class Instance { + constructor(private _schema: Schema, + private _fields: T, + private _options: InstanceOptions) { + } + + set fields(value: T) { + this._fields = value + } + + get fields(): T { + return this._fields + } + + public delete() { + const db = FileUtils.readJson("./database/db.json") + const filtered_table = db[this._schema.name] + .filter(row => row._id !== this._fields._id) + FileUtils.writeJson("./database/db.json", { + ...db, + [this._schema.name]: filtered_table + }) + } + + public save() { + const db = FileUtils.readJson("./database/db.json") + + if (!this._options.isNew) { + const filtered_table = db[this._schema.name] + .filter(row => row._id !== this._fields._id) + + new ColumnsUtils(this._schema.columns, filtered_table, this._fields) + filtered_table.push(this._fields) + FileUtils.writeJson("./database/db.json", { + ...db, + [this._schema.name]: filtered_table + } + ) + } else { + const table = db[this._schema.name] ?? [] + + new ColumnsUtils(this._schema.columns, table, this._fields) + table.push({...this._fields, _id: crypto.randomUUID()}) + FileUtils.writeJson("./database/db.json", { + ...db, + [this._schema.name]: table + } + ) + } + } +} diff --git a/src/Model.ts b/src/Model.ts new file mode 100644 index 0000000..1dd2f52 --- /dev/null +++ b/src/Model.ts @@ -0,0 +1,52 @@ +import {Schema, Instance} from "./mod.ts" +import {FileUtils, ObjectUtils} from "./utils/mod.ts" + +export class Model { + constructor( + private schema: Schema + ) { + } + + public create(args: T) { + return new Instance(this.schema, args, {isNew: true}) + } + + public findById(_id: string) { + const db = FileUtils.readJson("./database/db.json") + const table = db[this.schema.name] ?? [] + const candidate = table.find(row => row._id === _id) + + if (candidate === undefined) { + throw new Error("No record found") + } + + return new Instance(this.schema, candidate, { + isNew: false + }) + } + + public find(args: Partial) { + const db = FileUtils.readJson("./database/db.json") + const table = db[this.schema.name] ?? [] + const keys = Object.keys(args) as unknown as Array + + const filtered_table = table.filter(row => + keys.every(key => + ObjectUtils.nestedCheck(row, key, args[key]))) + + return filtered_table.map(row => new Instance(this.schema, row, { + isNew: false + })) + } + + public findOne(args: Partial) { + return this.find(args)[0] + } + + public findAll() { + const db = FileUtils.readJson("./database/db.json") + return db[this.schema.name].map(row => new Instance(this.schema, row, { + isNew: false + })) + } +} diff --git a/src/Schema.ts b/src/Schema.ts new file mode 100644 index 0000000..4478d02 --- /dev/null +++ b/src/Schema.ts @@ -0,0 +1,29 @@ +import {getFormat} from "./decorators/mod.ts"; +import {ColumnRules} from "./interfaces/IColumn.ts"; + +export class Schema { + constructor( + private _name: string = '', + private _columns: ColumnRules[] = [] + ) { + } + + get name(): string { + return this._name; + } + + get columns(): ColumnRules[] { + return this._columns; + } + + public static initializeSchema(args: T ) { + const instance = new (args as any)() + + const keys = Object.keys(instance).filter(key => key !== "_table_name" && key !== "_id"); + const options = keys.map(key => ({ + name: key, options: getFormat(instance, key) + })) + + return new Schema(instance["_table_name"], options); + } +} diff --git a/src/Table.ts b/src/Table.ts new file mode 100644 index 0000000..c9a777d --- /dev/null +++ b/src/Table.ts @@ -0,0 +1,28 @@ +import {FileUtils} from "./utils/mod.ts"; + +export class Table { + public static async init(name: string) { + try { + const isExists = await FileUtils.isFileExists("./database/db.json"); + + if (isExists) { + return this; + } + + await FileUtils.createOrCheckDir("./database"); + await FileUtils.writeJson("./database/db.json", {[name]: []}); + return this; + } catch (e) { + console.error(e.message); + } + } + + public static async nuke(name: string) { + try { + await FileUtils.writeJson("./database/db.json", {[name]: []}); + return this; + } catch (e) { + console.error(e.message); + } + } +} diff --git a/src/classes/Document.ts b/src/classes/Document.ts new file mode 100644 index 0000000..14abea5 --- /dev/null +++ b/src/classes/Document.ts @@ -0,0 +1,4 @@ +export class Document { + _id!: string; + _table_name!: string; +} diff --git a/src/classes/mod.ts b/src/classes/mod.ts new file mode 100644 index 0000000..74de6f2 --- /dev/null +++ b/src/classes/mod.ts @@ -0,0 +1 @@ +export {Document} from "./Document.ts"; diff --git a/src/decorators/Column.ts b/src/decorators/Column.ts new file mode 100644 index 0000000..17a77a3 --- /dev/null +++ b/src/decorators/Column.ts @@ -0,0 +1,16 @@ +import {Reflect} from "https://deno.land/x/reflect_metadata@v0.1.12/mod.ts"; +import {ColumnProps} from "../interfaces/IColumn.ts"; + +const formatMetadataKey = Symbol("columns"); + +export function getFormat(target: unknown, propertyKey: string) { + return Reflect.getMetadata(formatMetadataKey, target, propertyKey); +} + +export function Column(options: ColumnProps) { + const optionsProxy = options.allowNull === undefined + ? {...options, allowNull: false} + : {...options, allowNull: true}; + + return Reflect.metadata(formatMetadataKey, optionsProxy); +} diff --git a/src/decorators/TinyTable.ts b/src/decorators/TinyTable.ts new file mode 100644 index 0000000..ce02847 --- /dev/null +++ b/src/decorators/TinyTable.ts @@ -0,0 +1,15 @@ +import {Table} from "../Table.ts"; + +export function TinyTable(name: string) { + return function }>(Constructor: T) { + return class extends Constructor { + _table_name: string = name; + _id!: string; + + constructor(...args: any[]) { + super(...args); + Table.init(name); + } + } + } +} diff --git a/src/decorators/mod.ts b/src/decorators/mod.ts new file mode 100644 index 0000000..d317450 --- /dev/null +++ b/src/decorators/mod.ts @@ -0,0 +1,2 @@ +export {getFormat, Column} from "./Column.ts" +export {TinyTable} from "./TinyTable.ts" diff --git a/src/errors/ErrorWithHint.ts b/src/errors/ErrorWithHint.ts new file mode 100644 index 0000000..7eda40c --- /dev/null +++ b/src/errors/ErrorWithHint.ts @@ -0,0 +1,8 @@ +export class ErrorWithHint extends Error { + constructor(message: string, hint: string) { + super(` + Message: ${message} + Hint: ${hint} + `); + } +} diff --git a/src/errors/mod.ts b/src/errors/mod.ts new file mode 100644 index 0000000..5cdaa31 --- /dev/null +++ b/src/errors/mod.ts @@ -0,0 +1 @@ +export {ErrorWithHint} from "./ErrorWithHint.ts"; diff --git a/src/interfaces/IColumn.ts b/src/interfaces/IColumn.ts new file mode 100644 index 0000000..e6de564 --- /dev/null +++ b/src/interfaces/IColumn.ts @@ -0,0 +1,13 @@ +export type OptionTypes = "string" | "number" | "boolean" | "date" | "json" | "array"; + +export interface ColumnProps { + unique?: boolean; + type?: OptionTypes; + allowNull?: boolean; + default?: any; +} + +export interface ColumnRules { + name: string; + options: ColumnProps; +} diff --git a/src/interfaces/mod.ts b/src/interfaces/mod.ts new file mode 100644 index 0000000..17e54ad --- /dev/null +++ b/src/interfaces/mod.ts @@ -0,0 +1 @@ +export type {OptionTypes, ColumnRules, ColumnProps} from './IColumn.ts' diff --git a/src/mod.ts b/src/mod.ts new file mode 100644 index 0000000..a5fa0ba --- /dev/null +++ b/src/mod.ts @@ -0,0 +1,4 @@ +export {Model} from "./Model.ts"; +export {Schema} from "./Schema.ts"; +export {Table} from "./Table.ts"; +export {Instance} from "./Instance.ts"; diff --git a/src/utils/ColumnsUtils.ts b/src/utils/ColumnsUtils.ts new file mode 100644 index 0000000..ec7d8af --- /dev/null +++ b/src/utils/ColumnsUtils.ts @@ -0,0 +1,77 @@ +import {ColumnRules, OptionTypes} from "../interfaces/mod.ts"; +import {ErrorWithHint} from "../errors/mod.ts"; +import {parse} from "https://deno.land/std@0.144.0/datetime/mod.ts"; + +export class ColumnsUtils { + constructor( + private columnRules: ColumnRules[] = [], + private table: Array = [], + private record: T + ) { + this.run(); + } + + private checkType(value: T[keyof T], columnName: string, checkType?: OptionTypes) { + const valueType = typeof value; + + if (checkType === "date") { + try { + return parse(String(value), "yyyy-MM-dd"); + } catch (_) { + throw new Error(`${columnName} must be data`); + } + } + + if (checkType === "array") { + if (!Array.isArray(value)) throw new Error(`${columnName} must be array`); + return; + } + + if (checkType === "json") { + if (typeof value === "object" && !Array.isArray(value) && value !== null) return; + throw new Error(`${columnName} must be json`); + } + + if (valueType !== checkType) { + throw new Error(`${columnName} must be ${checkType}`); + } + } + + private unique(column: ColumnRules) { + const name = column.name as keyof T; + + if (this.table.some(row => row[name] === this.record[name])) { + throw new ErrorWithHint( + `${String(name)} must be unique`, + `${this.record[name]} is already exists` + ); + } + } + + private type(column: ColumnRules) { + const name = column.name as keyof T; + const value = this.record[name]; + return this.checkType(value, String(name), column.options.type); + } + + private default(column: ColumnRules) { + const name = column.name as keyof T; + this.record[name] = column.options.default; + } + + private run() { + this.columnRules.map(rule => { + if (rule.options.allowNull && + this.record[rule.name as keyof T] === undefined && + rule.options.default === undefined) return; + + + if ( + rule.options.default !== undefined && + this.record[rule.name as keyof T] === undefined) this.default(rule); + + rule.options.type !== undefined && this.type(rule); + rule.options.unique !== undefined && this.unique(rule); + }); + } +} diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts new file mode 100644 index 0000000..5ad7ffb --- /dev/null +++ b/src/utils/FileUtils.ts @@ -0,0 +1,40 @@ +import { ensureDir } from "https://deno.land/std@0.78.0/fs/mod.ts"; + +export class FileUtils { + static writeJson(path: string, data: Record | Array): string { + try { + Deno.writeTextFileSync(path, JSON.stringify(data)); + return "Written to " + path; + } catch (e) { + return e.message; + } + } + + static async isFileExists(path: string): Promise { + try { + await Deno.stat(path); + return true; + } catch (e) { + console.log(e.message) + return false; + } + } + + static async createOrCheckDir(path: string): Promise { + try { + await ensureDir(path); + return "Created directory " + path; + } catch (e) { + return e.message; + } + } + + static readJson(path: string): Record> { + try { + const data = Deno.readTextFileSync(path); + return JSON.parse(data); + } catch (_e) { + return {}; + } + } +} diff --git a/src/utils/ObjectUtils.ts b/src/utils/ObjectUtils.ts new file mode 100644 index 0000000..99abd2a --- /dev/null +++ b/src/utils/ObjectUtils.ts @@ -0,0 +1,26 @@ +export class ObjectUtils { + static nestedCheck>(obj: T, key: keyof T, value: any): boolean { + + if (typeof obj[key] === "boolean" && typeof value === "boolean") { + return obj[key] === value + } + + if (obj[key] === value) { + return true + } else { + return Object + .keys(value) + .every(inner_key => { + if ( + typeof obj[key][inner_key] === "object" && + !Array.isArray(obj[key][inner_key]) && + obj[key][inner_key] !== null + ) { + return this.nestedCheck(obj[key], inner_key, value[inner_key]) + } + + return obj[key][inner_key] === value[inner_key] + }) + } + } +} diff --git a/src/utils/mod.ts b/src/utils/mod.ts new file mode 100644 index 0000000..61ff528 --- /dev/null +++ b/src/utils/mod.ts @@ -0,0 +1,3 @@ +export {ColumnsUtils} from './ColumnsUtils.ts' +export {FileUtils} from './FileUtils.ts' +export {ObjectUtils} from './ObjectUtils.ts' diff --git a/tests/mod.ts b/tests/mod.ts new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..15ec3dc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "strictPropertyInitialization": false + } +}