Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add model creation support #49

Merged
merged 3 commits into from
Aug 5, 2024
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
9 changes: 9 additions & 0 deletions src/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class Model extends BaseModel {
static globalScopes = {};
static pluginInitializers = {};
static _booted = {};
static resolver = null;

static query(trx = null) {
const instance = new this();
Expand Down Expand Up @@ -100,6 +101,10 @@ class Model extends BaseModel {

}

static setConnectionResolver(resolver) {
this.resolver = resolver;
}

initialize() {

}
Expand Down Expand Up @@ -193,6 +198,10 @@ class Model extends BaseModel {
}

getConnection() {
if (this.constructor.resolver) {
return this.constructor.resolver.getConnection(this.connection);
}

return sutando.connection(this.connection);
}

Expand Down
130 changes: 111 additions & 19 deletions src/sutando.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
const Knex = require('knex');
const QueryBuilder = require('./query-builder');
const { getRelationMethod, getScopeMethod, compose, getAttrMethod } = require('./utils');
const Attribute = require('./casts/attribute');

class sutando {
static manager = {};
static connections = {};
static connectorFactory = null;
static instance = null;

constructor() {
this.manager = {};
this.connections = {};
this.models = {};
}

static getInstance() {
if (this.instance === null) {
this.instance = new sutando();
}

return this.instance;
}

static connection(connection = null) {
return this.getConnection(connection);
return this.getInstance().getConnection(connection);
}

static setConnectorFactory(connectorFactory) {
Expand All @@ -18,12 +33,44 @@ class sutando {
return this.connectorFactory || Knex;
}

static getConnection(name = null) {
static addConnection(config, name = 'default') {
return this.getInstance().addConnection(config, name);
}

static beginTransaction(connection = null) {
return this.getInstance().beginTransaction(connection);
}

static transaction(callback, connection = null) {
return this.getInstance().transaction(callback, connection);
}

static table(name, connection = null) {
return this.getInstance().table(name, connection);
}

static schema(connection = null) {
return this.getInstance().schema(connection);
}

static async destroyAll() {
await this.getInstance().destroyAll();
}

static createModel(name, options) {
return this.getInstance().createModel(name, options);
}

connection(connection = null) {
return this.getConnection(connection);
}

getConnection(name = null) {
name = name || 'default';
if (this.manager[name] === undefined) {
const queryBuilder = new QueryBuilder(
this.connections[name],
this.getConnectorFactory()
this.constructor.getConnectorFactory()
);

this.manager[name] = queryBuilder;
Expand All @@ -32,7 +79,7 @@ class sutando {
return this.manager[name];
}

static addConnection(config, name = 'default') {
addConnection(config, name = 'default') {
this.connections[name] = {
...config,
connection: {
Expand All @@ -48,35 +95,80 @@ class sutando {
};
}

static beginTransaction(connection = null) {
beginTransaction(connection = null) {
return this.connection(connection).transaction();
}

static transaction(callback, connection = null) {
transaction(callback, connection = null) {
return this.connection(connection).transaction(callback);
}

static commit(connection = null) {

}

static rollback(connection = null) {

}

static table(name, connection = null) {
table(name, connection = null) {
return this.connection(connection).table(name);
}

static schema(connection = null) {
schema(connection = null) {
return this.connection(connection).schema;
}

static async destroyAll() {
async destroyAll() {
await Promise.all(Object.values(this.manager).map((connection) => {
return connection?.destroy();
}));
}

createModel(name, options = {}) {
const Model = require('./model');
let BaseModel = Model;
if ('plugins' in options) {
BaseModel = compose(BaseModel, ...options.plugins);
}

this.models = {
...this.models,
[name]: class extends BaseModel {
table = options?.table ?? null;
connection = options?.connection ?? null;
timestamps = options?.timestamps ?? true;
primaryKey = options?.primaryKey ?? 'id';
keyType = options?.keyType ?? 'int';
incrementing = options?.incrementing ?? true;
with = options?.with ?? [];
casts = options?.casts ?? {};

static CREATED_AT = options?.CREATED_AT ?? 'created_at';
static UPDATED_AT = options?.UPDATED_AT ?? 'updated_at';
static DELETED_AT = options?.DELETED_AT ?? 'deleted_at';
}
}

if ('attributes' in options) {
for (const attribute in options.attributes) {
if (options.attributes[attribute] instanceof Attribute === false) {
throw new Error(`Attribute must be an instance of "Attribute"`);
}

this.models[name].prototype[getAttrMethod(attribute)] = () => options.attributes[attribute];
}
}

if ('relations' in options) {
for (const relation in options.relations) {
this.models[name].prototype[getRelationMethod(relation)] = function () {
return options.relations[relation](this);
};
}
}

if ('scopes' in options) {
for (const scope in options.scopes) {
this.models[name].prototype[getScopeMethod(scope)] = options.scopes[scope];
}
}

this.models[name].setConnectionResolver(this);
return this.models[name];
}
}

module.exports = sutando;
91 changes: 89 additions & 2 deletions test/index.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const unset = require('lodash/unset');
const filter = require('lodash/filter');
const kebabCase = require('lodash/kebabCase');
const { sutando, Model, Collection, Builder, Paginator, compose, SoftDeletes, Attribute, HasUniqueIds, CastsAttributes, ModelNotFoundError } = require('../src');
const config = require(process.env.SUTANDO_CONFIG || './config');
const dayjs = require('dayjs');
Expand Down Expand Up @@ -72,23 +73,50 @@ describe('Model', () => {
class Thumbnail extends Model {}
class Media extends Model {}

const manager = new sutando;

manager.createModel('User', {
plugins: [SomePlugin],
relations: {
post: (model) => model.hasMany(manager.models.Post),
}
});

manager.createModel('Post', {
relations: {
author: (model) => model.belongsTo(manager.models.User),
tags: (model) => model.belongsToMany(Tag, 'post_tag'),
thumbnail: (model) => model.belongsTo(Thumbnail, 'thumbnail_id'),
}
});

it('return the table name of the plural model name', () => {
const user = new User;
const media = new Media;
expect(user.getTable()).toBe('users');
expect(media.getTable()).toBe('media');

const anotherUser = new manager.models.User;
expect(anotherUser.getTable()).toBe('users');
});

describe('#compose', () => {
it('should return a Model instance', () => {
const user = new User;
expect(user).toBeInstanceOf(Model);

const anotherUser = new manager.models.User;
expect(anotherUser).toBeInstanceOf(Model);
});

it('has mixin\'s attributes and methods', () => {
const user = new User;
expect(user.pluginAttribtue).toBe('plugin');
expect(user.pluginMethod()).toBe('plugin');

const anotherUser = new manager.models.User;
expect(anotherUser.pluginAttribtue).toBe('plugin');
expect(anotherUser.pluginMethod()).toBe('plugin');
})
})

Expand Down Expand Up @@ -367,6 +395,31 @@ describe('Integration test', () => {
}
}

sutando.createModel('Post', {
connection: config.client,
attributes: {
slug: Attribute.make({
get: (value, attributes) => kebabCase(attributes.name)
})
},
scopes: {
idOf: (query, id) => query.where('id', id),
publish: (query) => query.where('status', 1)
},
relations: {
author: (model) => model.belongsTo(User),
default_author: (model) => model.belongsTo(User).withDefault({
name: 'Default Author'
}),
default_post_author: (model) => model.belongsTo(User).withDefault((user, post) => {
user.name = post.name + ' - Default Author';
}),
thumbnail: (model) => model.belongsTo(Media, 'thumbnail_id'),
media: (model) => model.belongsToMany(Media),
tags: (model) => model.belongsToMany(Tag),
}
});

class Tag extends Base {
relationPosts() {
return this.belongsToMany(Post);
Expand Down Expand Up @@ -1016,10 +1069,15 @@ describe('Integration test', () => {

describe('#save()', () => {
it('saves a new object', async () => {
const count = await Post.query().count();
expect(count).toBe(6);

const post = new Post;
post.user_id = 0;
post.name = 'Fourth post';
await post.save();
const a = await post.save();
const count1 = await Post.query().count();
expect(count1).toBe(7);

expect(Number(post.id)).toBe(7);

Expand Down Expand Up @@ -1049,7 +1107,6 @@ describe('Integration test', () => {
name: 'A Cool Blog',
});
post.user_id = 1;

await post.save();
expect(post.toData()).toHaveProperty('name', 'A Cool Blog');
expect(post.toData()).toHaveProperty('user_id', 1);
Expand Down Expand Up @@ -1391,6 +1448,36 @@ describe('Integration test', () => {
expect(post.attributes.custom_cast).toBe('{"a":"bar","b":"foo"}');
});
});

describe('#createModel', () => {
it('scopes/attributes/relations', async () => {
const { Post } = sutando.instance.models;
let posts = await Post.query().idOf(3).get();
expect(posts.modelKeys()).toEqual([3]);

posts = await Post.query().idOf(3).orWhere(q => {
q.idOf(4);
}).get();
expect(posts.modelKeys()).toEqual([3, 4]);

let post = await Post.query().with('default_author').find(4);
let xpost = post.toData();
unset(xpost, 'created_at');
unset(xpost, 'updated_at');

expect(post.default_author).toBeInstanceOf(User);
expect(xpost).toEqual({
id: 4,
user_id: 30,
name: 'This is a new Title 4!',
content: 'Lorem ipsum Anim sed eu sint aute.',
default_author: {
name: 'Default Author'
}
});
expect(post.slug).toBe('this-is-a-new-title-4');
});
});
});

describe('Relation', () => {
Expand Down
Loading
Loading