Skip to content

Commit

Permalink
feat: first commit πŸŽ‰πŸŽ‰
Browse files Browse the repository at this point in the history
  • Loading branch information
yUnreal committed May 25, 2024
1 parent 27e5e3e commit ada7e01
Show file tree
Hide file tree
Showing 48 changed files with 379 additions and 1,328 deletions.
33 changes: 33 additions & 0 deletions lib/drivers/JSONDriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { AnyObject } from '../types/utils';
import { dirname } from 'path';
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';

export class JSONDriver<S extends AnyObject> {
public constructor(public readonly path: string) {
if (!path.endsWith('.json'))
throw new Error('Invalid path', { cause: path });
if (!existsSync(path)) {
const dir = dirname(path);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true }); // Cria o diretΓ³rio recursivamente
}
writeFileSync(path, '{}', 'utf8');
}
}

public read() {
const stringifiedData = readFileSync(this.path, 'utf8');

return <Record<string, S>>JSON.parse(stringifiedData);
}

public update(fn: (data: Record<string, S>) => unknown) {
const data = this.read();

fn(data);

writeFileSync(this.path, JSON.stringify(data, null, '\t') + '\n');

return this;
}
}
8 changes: 8 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Model } from './structs/Model';
import { Schema } from './structs/schema/Schema';
import { AnyObject } from './types/utils';

export const createModel = <S extends Schema<AnyObject>>(
name: string,
schema: S
) => new Model(name, schema);
18 changes: 18 additions & 0 deletions lib/structs/Document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { AnyObject } from '../types/utils';
import { Model } from './Model';
import { Schema } from './schema/Schema';

export class Document<T extends AnyObject & { id?: string }> {
public constructor(
public data: T,
public model: Model<Schema<T>>
) {}

public fetch() {
return this.model.findById(this.id);
}

public get id() {
return <string>this.data.id;
}
}
55 changes: 55 additions & 0 deletions lib/structs/Model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { JSONDriver } from '../drivers/JSONDriver';
import { Infer } from '../types/schema';
import { AnyObject } from '../types/utils';
import { Document } from './Document';
import { Schema } from './schema/Schema';
import uuid from 'uuid-random';
import { join } from 'path';

export class Model<S extends Schema<AnyObject>> {
public driver: JSONDriver<Infer<S>>;

public constructor(
public name: string,
public schema: S
) {
this.driver = new JSONDriver<Infer<S>>(
join(__dirname, `../../wasp/${this.name}/${this.name}.json`)
);
}

public create(data: Infer<S> & { id?: string }) {
if ('id' in data && typeof data.id !== 'string')
throw new Error('"id" property must always be a string');

this.schema.parse(data);

data.id ??= uuid();

this.driver.update((crrData) =>
Object.defineProperty(crrData, <string>data.id, {
value: data,
enumerable: true,
})
);

return new Document(data, this);
}

public findById(id: string) {
const doc = this.driver.read()[id];

if (!doc) return null;

// @ts-expect-error Ignore it
return new Document<Infer<S>>(doc, this);
}

public findByIdAndDelete(id: string) {
const doc = this.findById(id);

if (doc) this.driver.update((data) => delete data[doc.id]);

return doc;
}
}
4 changes: 4 additions & 0 deletions lib/structs/schema/BooleanSchemaKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Types } from '../../types/schema';
import { SchemaKey } from './SchemaKey';

export class BooleanSchemaKey extends SchemaKey<Types.Boolean> {}
40 changes: 40 additions & 0 deletions lib/structs/schema/NumberSchemaKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Types, Expression } from '../../types/schema';
import { SchemaKey } from './SchemaKey';

export class NumberSchemaKey extends SchemaKey<Types.Number> {
public integer(message = `"${Expression.Key}" must be a integer`) {
return this.effect(Number.isSafeInteger, message);
}

public float(message = `"${Expression.Key}" me be a float`) {
return this.effect((num) => !Number.isInteger(num), message);
}

public min(
number: number,
message = `Min length of the "${Expression.Key}" must be ${number}`
) {
return this.effect((num) => num > number, message);
}

public max(
number: number,
message = `Max length of the "${Expression.Key}" must be ${number}`
) {
return this.effect((num) => num < number, message);
}

public gte(
number: number,
message = `"${Expression.Key}" must be greater or equal than ${number}`
) {
return this.min(number - 1, message);
}

public lte(
number: number,
message = `"${Expression.Key}" must be less or equal to ${number}`
) {
return this.max(number - 1, message);
}
}
58 changes: 58 additions & 0 deletions lib/structs/schema/Schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import isPlainObject from 'is-plain-obj';
import { AnyObject } from '../../types/utils';
import { InferShape, SchemaKeyFlags, Types } from '../../types/schema';
import { StringSchemaKey } from './StringSchemaKey';
import { NumberSchemaKey } from './NumberSchemaKey';
import { BooleanSchemaKey } from './BooleanSchemaKey';

export class Schema<S extends AnyObject> {
public constructor(public shape: Required<InferShape<S>>) {}

public static string() {
return new StringSchemaKey({ type: Types.String, flags: [] });
}

public static number() {
return new NumberSchemaKey({ type: Types.Number, flags: [] });
}

public static boolean() {
return new BooleanSchemaKey({ type: Types.Boolean, flags: [] });
}

public parse<O>(object: O) {
if (!isPlainObject(object))
throw new Error('Invalid value when parsing schema');

const requiredKeys = this.getRequiredKeys();
const missingKey = requiredKeys.find(
(key) => !Object.keys(object).includes(key)
);

if (missingKey) throw new Error(`Key "${missingKey}" is missing`);

for (const [key, value] of Object.entries(object)) {
if (key === 'id') continue;

const definition = this.shape[key];

if (!definition) throw new Error(`Unknown key "${key}"`);
if (
value === undefined ||
(value === null &&
definition.options.flags.includes(SchemaKeyFlags.Nullable))
)
continue;

definition.parse(value, key);
}

return object;
}

public getRequiredKeys() {
return Object.keys(this.shape).filter(
(key) => this.shape[key].required
);
}
}
61 changes: 61 additions & 0 deletions lib/structs/schema/SchemaKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
Constraint,
MappedSchemaTypes,
SchemaKeyFlags,
SchemaKeyOptions,
Types,
} from '../../types/schema';
import { getType } from '../../utils/getType';

export abstract class SchemaKey<Type extends Types> {
protected constraints = <Constraint<Type>[]>[];

public constructor(public options: SchemaKeyOptions<Type>) {}

public get type() {
return this.options.type;
}

public get required() {
return !this.options.flags.includes(SchemaKeyFlags.Optional);
}

public nullable() {
const { flags } = this.options;

if (flags.includes(SchemaKeyFlags.Nullable)) return this;

flags.push(SchemaKeyFlags.Nullable);

return this;
}

public optional() {
const { flags } = this.options;

if (flags.includes(SchemaKeyFlags.Optional)) return this;

flags.push(SchemaKeyFlags.Optional);

return this;
}

public effect(
effect: (value: MappedSchemaTypes[Type]) => unknown,
message: string
) {
this.constraints.push({ effect, message });

return this;
}

public parse(value: unknown, key: string) {
if (getType(value) !== this.type)
throw new Error(`Invalid value, expected the type "${this.type}"`);

for (const { effect, message } of this.constraints) {
// @ts-expect-error Will be fixed as soon as possible
if (!effect(value)) throw new Error(message.replace('{KEY}', key));
}
}
}
25 changes: 25 additions & 0 deletions lib/structs/schema/StringSchemaKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Expression, Types } from '../../types/schema';
import { SchemaKey } from './SchemaKey';

export class StringSchemaKey extends SchemaKey<Types.String> {
public min(
number: number,
message = `"${Expression.Key}" length must be greater than ${number}`
) {
return this.effect((string) => string.length >= number, message);
}

public max(
number: number,
message = `"${Expression.Key}" length must be less than ${number}`
) {
return this.effect((string) => string.length <= number, message);
}

public match(
pattern: RegExp,
message = `"${Expression.Key}" must match the pattern "${pattern.source}"`
) {
return this.effect((string) => pattern.test(string), message);
}
}
62 changes: 62 additions & 0 deletions lib/types/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { BooleanSchemaKey } from '../structs/schema/BooleanSchemaKey';
import { NumberSchemaKey } from '../structs/schema/NumberSchemaKey';
import { Schema } from '../structs/schema/Schema';
import { StringSchemaKey } from '../structs/schema/StringSchemaKey';
import { AnyObject } from './utils';

export enum Types {
String,
Number,
Boolean,
}

export type SchemaDefinition = {
[K: string]: SchemaKeyOptions<Types> | SchemaDefinition;
};

export interface SchemaKeyOptions<Type extends Types> {
type: Type;
flags: SchemaKeyFlags[];
}

export enum SchemaKeyFlags {
Nullable = 'NULLABLE',
Optional = 'OPTIONAL',
}

export interface MappedSchemaTypes {
[Types.String]: string;
[Types.Number]: number;
[Types.Boolean]: boolean;
}

export interface MappedSchemaKeys {
[Types.String]: StringSchemaKey;
[Types.Number]: NumberSchemaKey;
[Types.Boolean]: BooleanSchemaKey;
}

export type InferShape<S extends AnyObject> = {
[K in keyof S]: MappedSchemaKeys[ExtractType<S[K]>];
};

export type ExtractType<S> = S extends string
? Types.String
: S extends number
? Types.Number
: S extends boolean
? Types.Boolean
: never;

export interface Constraint<Type extends Types> {
effect(value: MappedSchemaTypes[Type]): unknown;
message: string;
}

export enum Expression {
Key = '{KEY}',
}

export type Infer<S extends Schema<AnyObject>> = S extends Schema<infer Shape>
? Shape
: never;
1 change: 1 addition & 0 deletions lib/types/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type AnyObject = Record<string, any>;
14 changes: 14 additions & 0 deletions lib/utils/getType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Types } from '../types/schema';

export const getType = (value: unknown) => {
switch (typeof value) {
case 'number':
return Types.Number;
case 'string':
return Types.String;
case 'boolean':
return Types.Boolean;
default:
throw new Error('Could not get the type from the value');
}
};
Loading

0 comments on commit ada7e01

Please sign in to comment.