From 305fb03d81d6d291fc8f097c368e6646ba818399 Mon Sep 17 00:00:00 2001 From: Donald Pakkies Date: Sun, 26 Oct 2025 12:24:15 +0200 Subject: [PATCH 1/6] feat: added belongsTo, hasOne, hasMany and belongsToMany --- src/Database/Database.imba | 307 ++++++++++++++++++++++++++++++++++- src/Database/Repository.imba | 20 +++ 2 files changed, 326 insertions(+), 1 deletion(-) diff --git a/src/Database/Database.imba b/src/Database/Database.imba index 3047c7f6..b20294a7 100644 --- a/src/Database/Database.imba +++ b/src/Database/Database.imba @@ -3,6 +3,7 @@ import { attachPaginate } from 'knex-paginate' import querystring from 'querystring' import location from '../Support/Helpers/location' import isString from '../Support/Helpers/isString' +import singularize from '../Support/Helpers/singularize' import Config from './Config' import knex from 'knex' @@ -37,13 +38,16 @@ try for column in columns object[column] = result[column] mappedResults.push(object) - return mappedResults + results = mappedResults if this._hidden && Array.isArray(this._hidden) && this._hidden.length > 0 for result in results for column in this._hidden delete result[column] + if this._relationships && Array.isArray(this._relationships) && this._relationships.length > 0 + results = await this._loadRelationships(results) + results knex.QueryBuilder.extend 'autoPaginate', do(pageSize = 20) @@ -109,6 +113,9 @@ try for column in this._hidden delete result[column] + if this._relationships && Array.isArray(this._relationships) && this._relationships.length > 0 + data = await this._loadRelationships(data) + const results = { data, pagination: { @@ -190,6 +197,304 @@ try return this + knex.QueryBuilder.extend 'belongsTo', do(relatedTable, queryCallback, foreignKey, localKey) + this._relationships = this._relationships || [] + + let tableName = relatedTable + if typeof relatedTable == 'function' && relatedTable.prototype && relatedTable.prototype.tableName + tableName = new relatedTable().tableName + + if typeof queryCallback == 'function' + this._relationships.push({ + type: 'belongsTo', + relatedTable: tableName, + queryCallback: queryCallback, + foreignKey: foreignKey || singularize(tableName) + '_id', + localKey: localKey || 'id' + }) + else if typeof queryCallback == 'string' + this._relationships.push({ + type: 'belongsTo', + relatedTable: tableName, + queryCallback: null, + foreignKey: queryCallback, + localKey: foreignKey || 'id' + }) + else + this._relationships.push({ + type: 'belongsTo', + relatedTable: tableName, + queryCallback: null, + foreignKey: singularize(tableName) + '_id', + localKey: 'id' + }) + + return this + + knex.QueryBuilder.extend 'hasOne', do(relatedTable, queryCallback, foreignKey, localKey) + this._relationships = this._relationships || [] + + let tableName = relatedTable + if typeof relatedTable == 'function' && relatedTable.prototype && relatedTable.prototype.tableName + tableName = new relatedTable().tableName + + if typeof queryCallback == 'function' + this._relationships.push({ + type: 'hasOne', + relatedTable: tableName, + queryCallback: queryCallback, + foreignKey: foreignKey || singularize(this._single.table) + '_id', + localKey: localKey || 'id' + }) + else if typeof queryCallback == 'string' + this._relationships.push({ + type: 'hasOne', + relatedTable: tableName, + queryCallback: null, + foreignKey: queryCallback, + localKey: foreignKey || 'id' + }) + else + this._relationships.push({ + type: 'hasOne', + relatedTable: tableName, + queryCallback: null, + foreignKey: singularize(this._single.table) + '_id', + localKey: 'id' + }) + + return this + + knex.QueryBuilder.extend 'hasMany', do(relatedTable, queryCallback, foreignKey, localKey) + this._relationships = this._relationships || [] + + let tableName = relatedTable + if typeof relatedTable == 'function' && relatedTable.prototype && relatedTable.prototype.tableName + tableName = new relatedTable().tableName + + if typeof queryCallback == 'function' + this._relationships.push({ + type: 'hasMany', + relatedTable: tableName, + queryCallback: queryCallback, + foreignKey: foreignKey || singularize(this._single.table) + '_id', + localKey: localKey || 'id' + }) + else if typeof queryCallback == 'string' + this._relationships.push({ + type: 'hasMany', + relatedTable: tableName, + queryCallback: null, + foreignKey: queryCallback, + localKey: foreignKey || 'id' + }) + else + this._relationships.push({ + type: 'hasMany', + relatedTable: tableName, + queryCallback: null, + foreignKey: singularize(this._single.table) + '_id', + localKey: 'id' + }) + + return this + + knex.QueryBuilder.extend 'belongsToMany', do(relatedTable, queryCallback, pivotTable, foreignKey, relatedKey, localKey, relatedLocalKey) + this._relationships = this._relationships || [] + + let tableName = relatedTable + if typeof relatedTable == 'function' && relatedTable.prototype && relatedTable.prototype.tableName + tableName = new relatedTable().tableName + + if typeof queryCallback == 'function' + this._relationships.push({ + type: 'belongsToMany', + relatedTable: tableName, + queryCallback: queryCallback, + pivotTable: pivotTable || singularize(this._single.table) + '_' + tableName, + foreignKey: foreignKey || singularize(this._single.table) + '_id', + relatedKey: relatedKey || singularize(tableName) + '_id', + localKey: localKey || 'id', + relatedLocalKey: relatedLocalKey || 'id' + }) + else if typeof queryCallback == 'string' + this._relationships.push({ + type: 'belongsToMany', + relatedTable: tableName, + queryCallback: null, + pivotTable: queryCallback, + foreignKey: foreignKey || singularize(this._single.table) + '_id', + relatedKey: relatedKey || singularize(tableName) + '_id', + localKey: localKey || 'id', + relatedLocalKey: relatedLocalKey || 'id' + }) + else + this._relationships.push({ + type: 'belongsToMany', + relatedTable: tableName, + queryCallback: null, + pivotTable: singularize(this._single.table) + '_' + tableName, + foreignKey: singularize(this._single.table) + '_id', + relatedKey: singularize(tableName) + '_id', + localKey: 'id', + relatedLocalKey: 'id' + }) + + return this + + knex.QueryBuilder.extend '_loadRelationships', do(results) + if !results || results.length == 0 + return results + + for relationship in this._relationships + let relationshipName = relationship.relatedTable + if relationship.type == 'belongsTo' || relationship.type == 'hasOne' + relationshipName = singularize(relationship.relatedTable) + + if relationship.type == 'belongsTo' + await this._loadBelongsTo(results, relationship, relationshipName) + else if relationship.type == 'hasOne' + await this._loadHasOne(results, relationship, relationshipName) + else if relationship.type == 'hasMany' + await this._loadHasMany(results, relationship, relationshipName) + else if relationship.type == 'belongsToMany' + await this._loadBelongsToMany(results, relationship, relationshipName) + + results + + knex.QueryBuilder.extend '_loadBelongsTo', do(results, relationship, relationshipName) + const hasForeignKey = results.length > 0 && results[0].hasOwnProperty(relationship.foreignKey) + + if !hasForeignKey + const ids = results.map(do(result) result.id).filter(do(id) id != null) + + if ids.length == 0 + for result in results + result[relationshipName] = null + return + + const foreignKeyResults = await Database(this._single.table) + .select('id', relationship.foreignKey) + .whereIn('id', ids) + + const foreignKeyMap = {} + for row in foreignKeyResults + foreignKeyMap[row.id] = row[relationship.foreignKey] + + for result in results + result[relationship.foreignKey] = foreignKeyMap[result.id] + + const foreignKeys = results.map(do(result) result[relationship.foreignKey]).filter(do(key) key != null) + + if foreignKeys.length == 0 + for result in results + result[relationshipName] = null + return + + let relatedQuery = Database(relationship.relatedTable).whereIn(relationship.localKey, foreignKeys) + + if relationship.queryCallback + relatedQuery = relationship.queryCallback(relatedQuery) + + const relatedResults = await relatedQuery + const relatedMap = {} + + for related in relatedResults + relatedMap[related[relationship.localKey]] = related + + for result in results + result[relationshipName] = relatedMap[result[relationship.foreignKey]] || null + + knex.QueryBuilder.extend '_loadHasOne', do(results, relationship, relationshipName) + const localKeys = results.map(do(result) result[relationship.localKey]).filter(do(key) key != null) + + if localKeys.length == 0 + for result in results + result[relationshipName] = null + return + + let relatedQuery = Database(relationship.relatedTable).whereIn(relationship.foreignKey, localKeys) + + if relationship.queryCallback + relatedQuery = relationship.queryCallback(relatedQuery) + + const relatedResults = await relatedQuery + const relatedMap = {} + + for related in relatedResults + relatedMap[related[relationship.foreignKey]] = related + + for result in results + result[relationshipName] = relatedMap[result[relationship.localKey]] || null + + knex.QueryBuilder.extend '_loadHasMany', do(results, relationship, relationshipName) + const localKeys = results.map(do(result) result[relationship.localKey]).filter(do(key) key != null) + + if localKeys.length == 0 + for result in results + result[relationshipName] = [] + return + + let relatedQuery = Database(relationship.relatedTable).whereIn(relationship.foreignKey, localKeys) + + if relationship.queryCallback + relatedQuery = relationship.queryCallback(relatedQuery) + + const relatedResults = await relatedQuery + const relatedMap = {} + + for related in relatedResults + const key = related[relationship.foreignKey] + if !relatedMap[key] + relatedMap[key] = [] + relatedMap[key].push(related) + + for result in results + result[relationshipName] = relatedMap[result[relationship.localKey]] || [] + + knex.QueryBuilder.extend '_loadBelongsToMany', do(results, relationship, relationshipName) + const localKeys = results.map(do(result) result[relationship.localKey]).filter(do(key) key != null) + + if localKeys.length == 0 + for result in results + result[relationshipName] = [] + return + + const pivotQuery = Database(relationship.pivotTable) + .whereIn(relationship.foreignKey, localKeys) + .select(relationship.foreignKey, relationship.relatedKey) + + const pivotResults = await pivotQuery + const pivotMap = {} + + for pivot in pivotResults + const key = pivot[relationship.foreignKey] + if !pivotMap[key] + pivotMap[key] = [] + pivotMap[key].push(pivot[relationship.relatedKey]) + + const allRelatedIds = [...new Set(pivotResults.map(do(pivot) pivot[relationship.relatedKey]))] + + if allRelatedIds.length == 0 + for result in results + result[relationshipName] = [] + return + + let relatedQuery = Database(relationship.relatedTable).whereIn(relationship.relatedLocalKey, allRelatedIds) + + if relationship.queryCallback + relatedQuery = relationship.queryCallback(relatedQuery) + + const relatedResults = await relatedQuery + const relatedMap = {} + + for related in relatedResults + relatedMap[related[relationship.relatedLocalKey]] = related + + for result in results + const relatedIds = pivotMap[result[relationship.localKey]] || [] + result[relationshipName] = relatedIds.map(do(id) relatedMap[id]).filter(do(item) item != null) + knex.QueryBuilder.extend 'softDelete', do this.update({ deleted_at: Database.fn.now! }) knex.QueryBuilder.extend 'restore', do this.update({ deleted_at: null }) diff --git a/src/Database/Repository.imba b/src/Database/Repository.imba index 3ae7f7b3..c0124ecf 100644 --- a/src/Database/Repository.imba +++ b/src/Database/Repository.imba @@ -343,3 +343,23 @@ export default class Repository const query = self.table! query.get.apply(query, args) + + static def belongsTo ...args + const query = self.query! + + query.belongsTo.apply(query, args) + + static def hasOne ...args + const query = self.query! + + query.hasOne.apply(query, args) + + static def hasMany ...args + const query = self.query! + + query.hasMany.apply(query, args) + + static def belongsToMany ...args + const query = self.query! + + query.belongsToMany.apply(query, args) From 535c6c3900b3668c2fa00741602e4042cf3a7d73 Mon Sep 17 00:00:00 2001 From: Donald Pakkies Date: Sun, 26 Oct 2025 12:25:11 +0200 Subject: [PATCH 2/6] chore: update types --- types/Database/Database.d.ts | 14 +++++++++++++- types/Database/Repository.d.ts | 12 ++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/types/Database/Database.d.ts b/types/Database/Database.d.ts index 814541fd..7e0c8a33 100644 --- a/types/Database/Database.d.ts +++ b/types/Database/Database.d.ts @@ -1,5 +1,6 @@ import "knex"; import type { Knex } from "knex"; +import type Repository from "../Database/Repository"; declare let Database: Knex; declare type Database = Knex; @@ -52,7 +53,18 @@ declare module "knex" { */ autoPaginate(perPage?: number): Promise>; hidden(columns: string[]): Knex.QueryBuilder; - hasOne(related: string, foreignKey: string, localKey: string): Knex.QueryBuilder; + belongsTo(related: string | typeof Repository): Knex.QueryBuilder; + belongsTo(related: string | typeof Repository, queryCallback: (query: Knex.QueryBuilder) => Knex.QueryBuilder): Knex.QueryBuilder; + belongsTo(related: string | typeof Repository, foreignKey: string, localKey: string): Knex.QueryBuilder; + hasOne(related: string | typeof Repository): Knex.QueryBuilder; + hasOne(related: string | typeof Repository, queryCallback: (query: Knex.QueryBuilder) => Knex.QueryBuilder): Knex.QueryBuilder; + hasOne(related: string | typeof Repository, foreignKey: string, localKey: string): Knex.QueryBuilder; + hasMany(related: string | typeof Repository): Knex.QueryBuilder; + hasMany(related: string | typeof Repository, queryCallback: (query: Knex.QueryBuilder) => Knex.QueryBuilder): Knex.QueryBuilder; + hasMany(related: string | typeof Repository, foreignKey: string, localKey: string): Knex.QueryBuilder; + belongsToMany(related: string | typeof Repository): Knex.QueryBuilder; + belongsToMany(related: string | typeof Repository, queryCallback: (query: Knex.QueryBuilder) => Knex.QueryBuilder): Knex.QueryBuilder; + belongsToMany(related: string | typeof Repository, pivotTable: string, foreignKey?: string, relatedKey?: string, localKey?: string, relatedLocalKey?: string): Knex.QueryBuilder; } interface TableBuilder { softDeletes(): Knex.TableBuilder; diff --git a/types/Database/Repository.d.ts b/types/Database/Repository.d.ts index c09d9957..759ebbbe 100644 --- a/types/Database/Repository.d.ts +++ b/types/Database/Repository.d.ts @@ -526,4 +526,16 @@ export default class Repository { static onlyTrashed(): Knex.QueryBuilder; static get(columns?: string[]): Promise; get(columns?: string[]): Promise; + static belongsTo(related: string | typeof Repository): Knex.QueryBuilder; + static belongsTo(related: string | typeof Repository, queryCallback: (query: Knex.QueryBuilder) => Knex.QueryBuilder): Knex.QueryBuilder; + static belongsTo(related: string | typeof Repository, foreignKey: string, localKey: string): Knex.QueryBuilder; + static hasOne(related: string | typeof Repository): Knex.QueryBuilder; + static hasOne(related: string | typeof Repository, queryCallback: (query: Knex.QueryBuilder) => Knex.QueryBuilder): Knex.QueryBuilder; + static hasOne(related: string | typeof Repository, foreignKey: string, localKey: string): Knex.QueryBuilder; + static hasMany(related: string | typeof Repository): Knex.QueryBuilder; + static hasMany(related: string | typeof Repository, queryCallback: (query: Knex.QueryBuilder) => Knex.QueryBuilder): Knex.QueryBuilder; + static hasMany(related: string | typeof Repository, foreignKey: string, localKey: string): Knex.QueryBuilder; + static belongsToMany(related: string | typeof Repository): Knex.QueryBuilder; + static belongsToMany(related: string | typeof Repository, queryCallback: (query: Knex.QueryBuilder) => Knex.QueryBuilder): Knex.QueryBuilder; + static belongsToMany(related: string | typeof Repository, pivotTable: string, foreignKey?: string, relatedKey?: string, localKey?: string, relatedLocalKey?: string): Knex.QueryBuilder; } From 1f6e2722dd412335e07b3b0aa88ccb5e54255dc2 Mon Sep 17 00:00:00 2001 From: Donald Pakkies Date: Sun, 26 Oct 2025 14:47:22 +0200 Subject: [PATCH 3/6] feat: add vite manifest reader --- src/Vite/Repository.imba | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/Vite/Repository.imba diff --git a/src/Vite/Repository.imba b/src/Vite/Repository.imba new file mode 100644 index 00000000..ab4904c6 --- /dev/null +++ b/src/Vite/Repository.imba @@ -0,0 +1,46 @@ +import { readFileSync, existsSync } from 'fs' +import { join } from 'path' + +export default class Repository + + static manifestCache\ViteManifest|null = null + + static def get file\string + const manifest = self.getManifest! + + if !manifest + return file + + const normalizedFile = file.startsWith('/') ? file.slice(1) : file + + if manifest[normalizedFile] + return '/build/' + manifest[normalizedFile].file + + if normalizedFile == 'css/app.css' + for [key, value] in Object.entries(manifest) + if key.includes('css/app.css') + return '/build/' + value.file + + if normalizedFile == 'js/app.js' + for [key, value] in Object.entries(manifest) + if key.includes('js/app.ts') + return '/build/' + value.file + + file + + static def getManifest\ViteManifest|null + if self.manifestCache != null + return self.manifestCache + + const location = join(process.cwd!, 'public', 'build', '.vite', 'manifest.json') + + try + if existsSync(location) + const content = readFileSync(location, 'utf8') + self.manifestCache = content ? JSON.parse(content) : null + else + self.manifestCache = null + catch err + self.manifestCache = null + + self.manifestCache From db9ce8651ad1441568f537710c62e4fd1662b5bf Mon Sep 17 00:00:00 2001 From: Donald Pakkies Date: Sun, 26 Oct 2025 14:47:43 +0200 Subject: [PATCH 4/6] feat: add vite helper --- src/Support/Helpers/index.imba | 2 ++ src/Support/Helpers/vite.imba | 4 ++++ 2 files changed, 6 insertions(+) create mode 100644 src/Support/Helpers/vite.imba diff --git a/src/Support/Helpers/index.imba b/src/Support/Helpers/index.imba index 00d15530..01a6a3fd 100644 --- a/src/Support/Helpers/index.imba +++ b/src/Support/Helpers/index.imba @@ -35,6 +35,7 @@ const { default: toBoolean } = require './toBoolean' const { default: updateLine } = require './updateLine' const { default: version } = require './version' const { default: view } = require './view' +const { default: vite } = require './vite' const { default: wildcard } = require './wildcard' const { default: without } = require './without' @@ -81,6 +82,7 @@ export { updateLine version view + vite wildcard without } diff --git a/src/Support/Helpers/vite.imba b/src/Support/Helpers/vite.imba new file mode 100644 index 00000000..258f3cf5 --- /dev/null +++ b/src/Support/Helpers/vite.imba @@ -0,0 +1,4 @@ +import Repository from '../../Vite/Repository' + +export default def vite file\string + Repository.get file From 3f2a159661edace90b068fb4893d7a352442f877 Mon Sep 17 00:00:00 2001 From: Donald Pakkies Date: Sun, 26 Oct 2025 14:48:20 +0200 Subject: [PATCH 5/6] feat: add vite helper to view class --- src/Http/View/View.imba | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/Http/View/View.imba b/src/Http/View/View.imba index d72ff7b0..24e7b27f 100644 --- a/src/Http/View/View.imba +++ b/src/Http/View/View.imba @@ -7,6 +7,7 @@ import isString from '../../Support/Helpers/isString' import querystring from 'querystring' import UndefinedDataPropException from './Exceptions/UndefinedDataPropException' import Language from '../../Support/Language/Language' +import viteHelper from '../../Support/Helpers/vite' export default class View @@ -36,6 +37,40 @@ export default class View self + def vite file\string|string[] + if Array.isArray(file) + let tags = [] + + for asset in file + if !isString(asset) + throw TypeError "Expected string." + + const jsTagExtensions = ['js', 'ts'] + const cssTagExtensions = ['css', 'scss', 'sass', 'less', 'styl', 'stylus'] + const imgTagExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'avif'] + const fontTagExtensions = ['woff', 'woff2', 'eot', 'ttf', 'otf'] + + const extension = asset.split('.').pop!.toLowerCase! + + if !isString(extension) + throw new Error "Could not determine file extension for: {asset}" + + if jsTagExtensions.includes(extension) + tags.push("") + else if cssTagExtensions.includes(extension) + tags.push("") + else if imgTagExtensions.includes(extension) + tags.push("") + else if fontTagExtensions.includes(extension) + tags.push("") + else + throw new Error "Unsupported file extension: {extension}" + + if tags.length > 0 + tags.join('\n') + else + viteHelper(file) + def translate key\string, default\any self.#_language.get(key, default) From 46083cbd43e3207acb050209887b3f6c6ae47046 Mon Sep 17 00:00:00 2001 From: Donald Pakkies Date: Sun, 26 Oct 2025 14:48:36 +0200 Subject: [PATCH 6/6] feat: update types --- types/Http/View/View.d.ts | 8 ++++++++ types/Support/Helpers/mix.d.ts | 7 +++++-- types/Support/Helpers/vite.d.ts | 7 +++++++ types/Vite/Repository.d.ts | 16 ++++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 types/Support/Helpers/vite.d.ts create mode 100644 types/Vite/Repository.d.ts diff --git a/types/Http/View/View.d.ts b/types/Http/View/View.d.ts index 359b369b..d0fd2672 100644 --- a/types/Http/View/View.d.ts +++ b/types/Http/View/View.d.ts @@ -31,6 +31,14 @@ export default class View { */ __(key: string, default$?: string): string; + /** + * Get asset path(s) from Vite manifest. + * + * @param {string | string[]} file + * @returns {string} + */ + vite(file: string | string[]): string; + /** * Get old input. */ diff --git a/types/Support/Helpers/mix.d.ts b/types/Support/Helpers/mix.d.ts index 293e0865..8c9d5d68 100644 --- a/types/Support/Helpers/mix.d.ts +++ b/types/Support/Helpers/mix.d.ts @@ -1,4 +1,7 @@ /** -@param {string} file -*/ + * Get asset path from Laravel Mix manifest. + * @param {string} file + * @returns {string} + * @deprecated + */ export default function mix(file: string): string; diff --git a/types/Support/Helpers/vite.d.ts b/types/Support/Helpers/vite.d.ts new file mode 100644 index 00000000..7e06aebc --- /dev/null +++ b/types/Support/Helpers/vite.d.ts @@ -0,0 +1,7 @@ +/** + * Get asset path(s) from Vite manifest. + * + * @param {string | string[]} file + * @returns {string} + */ +export default function vite(file: string | string[]): string; diff --git a/types/Vite/Repository.d.ts b/types/Vite/Repository.d.ts new file mode 100644 index 00000000..56040b83 --- /dev/null +++ b/types/Vite/Repository.d.ts @@ -0,0 +1,16 @@ +interface ViteManifest { + [key: string]: { + file: string; + src?: string; + isEntry?: boolean; + imports?: string[]; + css?: string[]; + assets?: string[]; + }; +} + +export default class Repository { + static manifestCache: ViteManifest | null = null; + static file(file: string): string; + static getManifest(): ViteManifest | null; +}