# Databless ## What we love in Databless ❤️ We love using it, various projects ❤️ Adapted easily [Databless readme on GitHub](https://github.com/AckeeCZ/databless/tree/next) ### Model - not defined attributes are filtered before inserting/updating ```javascript= export interface SubscriptionPlan { id: string type: SubscriptionPlanType price: number dishesCount: number name: string } export const subscriptionPlanModel = createModel<SubscriptionPlan>({ adapter: getKnex, collectionName: 'subscription_plans', attributes: { id: { type: 'string' }, price: { type: 'number', deserialize: value => (isNaN(Number(value)) ? null : Number(value)), }, type: { type: 'string' }, dishesCount: { type: 'number' }, name: { type: 'string' }, }, }) ``` - custom serialization ```javascript= objectStoredAsJson: { type: 'string', // serialize before inserting into a database serialize: x => JSON.stringify(x || null), // deserialize from database shape // deserialize can also be used to define attribute TS type, e.g. (x: string): MyEnum => x deserialize: x => JSON.parse(x), } ``` ### Repository - CRUD methods - `.create(data, options)` - `.createBulk(data[], options)` - `.detail(filters, options)` - `.delete(filters, options)` - `.list(filters, options)` - `.update(filters, data, options)` ```javascript= export const subscriptionPlanRepository = createRepository( subscriptionPlanModel ) ``` ### Filtering - via `filters` - exact match - `{ foo: 'bar' }` - where in - `{ foo: ['bar', 'baz'] }` - inequality (prefix `>`, `<`, `>=`, `<=`) - `{ foo: '>2' }` - searching (left & right wildcards) - `{ foo: '*abc*' }` - `{ foo: 'abc*' }` - `{ foo: '*abc' }` - custom queries - via `options.qb` parameter - be careful of SQL query - `{ qb: (qb: Knex.QueryBuilder) => qb.whereRaw('...') }` - custom model filters - do not overwrite default Model attribute filters - `addressRepository.list({ userId })` ```javascript= export const addressModel = createModel< Address, { customFilters: { userId: string } } >({ adapter: getKnex, collectionName: 'addresses', attributes: { id: { type: 'string' }, name: { type: 'string' }, street: { type: 'string' }, zip: { type: 'string' }, city: { type: 'string' }, countryCode: { type: 'string' }, note: { type: 'string' }, }, filters: { userId: (value: string, options) => { if (!value) { return } options.qb = composeQb(options.qb, qb => { return qb .join('user_addresses', 'addresses.id', 'user_addresses.address_id') .where('user_addresses.user_id', value) .orderBy('user_addresses.is_default', 'desc') }) }, }, }) ``` ### Counting - via `options.count` - `count: true` to get number of filtered records ### Ordering - via `options.order` - default - `{ order: 'foo' }` - asc - `{ order: '+foo' }` - desc - `{ order: '-foo' }` - order by multiple - `{ order: ['+foo', '-baz'] }` ### Pagination - via `options.limit` and `options.offset` - defaults: `limit=10, offset=0` - `{ limit: 10, offset: 0 }` ### Relations - attribute type of `relation` - example `AccountGroup` model ```javascript= accounts: { type: 'relation', targetModel: () => accountModel, relation: bookshelfRelation.createHasMany({ foreignKey: 'accountGroupId' }) }, ``` - related properties populated on `list` and `detail` repository methods within `withRelated` ```javascript= const book = bookRepository.detail({ id }, { withRelated: ['author' ] }) ``` ## What we hate in Databless - ❗ `createBulk` - does not return all items inserted ```javascript= [ { command: 'INSERT', rowCount: 2, oid: 0, rows: [], fields: [], parsers: undefined, types: TypeOverrides { _types: [Object], text: {}, binary: {} }, rowCtor: null, rowAsArray: false } ] ``` ```javascript= // workaround for getting entities you created (or you have to list) const [firstChef, secondChef] = await Promise.all([ userRepository.create({ fullName: 'test_chef_1', uid: 'test_chef_1', }), userRepository.create({ fullName: 'test_chef_2', uid: 'test_chef_2', }), ]) ``` - ❗ `delete` - do not know whether delete operation happened or not - ❗ `detail` - when `undefined` filter argument passed it returns the first item from a table - IRL example ```javascript= facebook: { getUserByEmail: email => getUser({ email }), }, .... const getUser = async ( params: Partial<AuthistUser> & { id?: string } ): Promise<AuthistUser | undefined> => { const user = await userRepository.detail(params, { withRelated: ['roles', 'settings'] }) if (!user) { return } .... ``` ![](https://i.imgur.com/ANN3f26.png) - ❗ `detail` - does not have return type of `underfined` or `null` - example with empty table `const x = await repository.detail({ id: '1' })` - ❗ `list` - when `undefined` filter argument passed it does returns all items - ❗ `update` - if item to be updated not found, it returns object with data to be updated - ❗ `camelCase` sometimes does not work - Model with `users` relation attribute example ```javascript= users: { type: 'relation', targetModel: () => userModel, relation: bookshelfRelation.createBelongsToMany({ joinTableName: 'subscriptionAddonsUsers', otherKey: 'userId', foreignKey: 'subscription_addon_id' }) } ``` - ❗ Missing properties on `AttributeRelation` type - such as `serialize / deserialize` properties - example `User` model with relation attribute returns TS error ```javascript= roles: { type: 'relation', targetModel: () => roleModel, relation: bookshelfRelation.createHasMany({ foreignKey: 'userId' }), // @ts-expect-error remove after databless fix - https://github.com/AckeeCZ/databless/issues/24 deserialize: (roles?: Array<{ role: string }>) => (roles ?? []).map(role => role.role) } ``` - ❗ Typescript support for nested queries within `withRelated` - example ```javascript= const users = await userRepository.list({}, { withRelated: [{ 'roles': function (qb: Knex.QueryBuilder) { qb.where('role', 'user'); } }] // as any }) ``` ``` error TS2322: Type '{ roles: (qb: QueryBuilder<any, any>) => void; }' is not assignable to type '"accountGroups" | "roles" | "settings"'. Type '{ roles: (qb: QueryBuilder<any, any>) => void; }' is not assignable to type '"settings"'. ``` - ❗ Typescript support for nested relations - `withRelated: ['order', 'destination', 'order.partner' as any]` - ❗ Typescript support for transactions in `RepositoryMethodOptions` ```javascript= const address = await addressRepository.create( { street: input.street, name: input.displayName, }, { transacting: tx } // have to type cast to any ) ``` ``` error TS2345: Argument of type '{ transacting: Knex.Transaction<any, any[]>; }' is not assignable to parameter of type 'RepositoryMethodOptions'. Object literal may only specify known properties, and 'transacting' does not exist in type 'RepositoryMethodOptions'. ``` - ❗ Redundant model interface - we define `attributes` types and also almost the same in Model `interface` - ❗ Circural reference - example on FlashNews - `dataChannel` entity has relation to `source` entity ```javascript= // dataChannel model source: { type: 'relation', targetModel: () => sourceModel, relation: bookshelfRelation.createBelongsTo({ foreignKey: 'sourceId', }), }, // source model producingDataChannels: { type: 'relation', targetModel: () => getDataChannelModel(), relation: bookshelfRelation.createHasMany({ foreignKey: 'sourceId', query: collection => collection.query(qb => { void qb .where('dataChannel.state', DataChannelState.Active) .andWhere('dataChannel.hideContent', false) }), }), }, // getter function for dataChannel model export const getDataChannelModel = (): Model<any> => dataChannelModel ``` ## What we should improve in Databless - General improvements to types. Some examples: - Correct return type for repository methods ```javascript const detail = async <E extends Entity, M extends Metadata<E>>( model: Model<E, M>, filter?: Filters<E, M>, options?: RepositoryDetailOptions<E, M> ): Promise<E | undefined> => {...}; const remove = async <E extends Entity, M extends Metadata<E>>( model: Model<E, M>, filter?: Filters<E, M>, options?: RepositoryMethodOptions): Promise<E | undefined> {...} ``` - Fix Type support for `columns` option when fetching the entity ```typescript export interface RepositoryMethodOptions { toJSON?: bookshelfUtil.SerializeOptions; qb?: bookshelfUtil.QbOption; columns?: string[]; } ``` - Type support for `transacting` option ```typescript export interface RepositoryMethodOptions { ..., transacting: any } ``` - Type for serialize and deserialize on Attribute relation ```typescript type AttributeRelation<R extends Relation = any> = { type: 'relation'; ... serialize?: (x: any) => PrimitiveToType<Primitive>; deserialize?: (x: any) => any; }; ``` - Correct types for "nested" `withRelated` options - Create bulk: correct return value/type ```typescript export const createBulk = async <E extends Entity, M extends Metadata<E>>( model: Model<E, M>, data: Array<Partial<NullishOptional<E>>>, options?: RepositoryMethodOptions ): Promise<E[]> => { const result = await model.options .adapter() .batchInsert( model.options.collectionName, data.map(dataItem => model.serialize(dataItem)) // TODO Support transactions using .transacting(trx) // TODO Add timestamps if bsModel.hasTimestamps ) .returning(options?.columns ?? '*'); return result as E[]; }; ``` - Proper return value/type for the delete function. In case we need to know if delete did happen or not - Update operation should return undefined if entity was not found - Camel casing - We should have it documented: where it does and does not work. - Defining models once instead of creating an Interface + Bookshelf model. As a example from other ORMs, we could define a single class to manage the model ```typescript= // objection.js export default class Animal extends Model { id!: number name!: string owner?: Person static tableName = 'animals' static jsonSchema = { type: 'object', required: ['name'], properties: { id: { type: 'integer' }, ownerId: { type: ['integer', 'null'] }, name: { type: 'string', minLength: 1, maxLength: 255 }, species: { type: 'string', minLength: 1, maxLength: 255 }, }, } static relationMappings = () => ({ owner: { relation: Model.BelongsToOneRelation, // The related model. modelClass: Person, join: { from: 'animals.ownerId', to: 'persons.id', }, }, }) } // MikroORM @Entity({tableName: 'books'}) export class Book extends CustomBaseEntity { @Property() title!: string; @ManyToOne(() => Author) author!: Author; @ManyToMany({ entity: 'BookTag', fixedOrder: true }) tags = new Collection<BookTag>(this); } ``` ## What we can add to Databless New repository methods: - Create a new instance without saving it to DB ```typescript create2(entityLike): Entity ``` - Upsert ```typescript upsert(entityLike: Partial<Entity> | Partial<Entity>[], options) Promise<Entity> ``` - Find and count ```typescript findAndCount(filters, options): Promise<[Entity[], count]> ``` - Soft delete and restore ```typescript softDelete(id, criteria): Promise<Entity> restore(id, criteria): Promise<Entity> ``` New features: - Cursor pagination support. We could use Relay cursor specification as a standard - Caching support via options parameter (memory or redis) ```typescript cache: { type: "redis" | "memory", options: { host: "localhost", port: 6379 } } ``` - Locking support via options parameter ```typescript { lockMode: LockMode.OPTIMISTIC, lockVersion: req.body.version } { lockMode: LockMode.PESSIMISTIC_WRITE,} ``` - Virtual properties - Lazy scalar properties ```typescript text: { type: 'string', lazy: true }, ``` - Events and Lifecycle Hooks ```typescript const User = model('User', { tableName: 'users', initialize() { this.on('updated', (model) => { // This is fired after a model is updated }) } }) ``` - Dataloader support for the relations ## Should we continue using Databless?
{"metaMigratedAt":"2023-06-16T20:45:01.034Z","metaMigratedFrom":"Content","title":"Databless","breaks":true,"contributors":"[{\"id\":\"c7b828ae-3e58-405d-9071-69503c09eb7f\",\"add\":9003,\"del\":376},{\"id\":\"6687b6a2-771a-4fd8-9f5d-120ab9b2f11a\",\"add\":15599,\"del\":10703}]"}
    110 views