From 1521c071ea487716d444a2305c5030aa725a79b8 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sat, 7 Mar 2026 17:37:40 +0100 Subject: [PATCH 1/2] fix: remove all non-null assertions to fix noNonNullAssertion lint warnings Replace 20 non-null assertion (!) usages across source and test files with safe alternatives: null guards, fallback values, and optional chaining. Add getSchemaInstance helper in test utils to centralize schema lookup. --- src/interfaces/data-api/beta/components.ts | 355 ++++++++---- src/interfaces/data-api/beta/metadata.ts | 223 ++++++-- .../beta/components/access_control.test.ts | 131 +++-- .../data-api/beta/components/listable.test.ts | 345 ++++++++---- .../beta/components/mandatory.test.ts | 167 ++++-- .../beta/components/per_action_access.test.ts | 84 ++- .../beta/components/validator.test.ts | 219 +++++--- .../data-api/beta/index/routes.test.ts | 194 +++++-- .../data-api/beta/integration.test.ts | 530 ++++++++++-------- src/test/interfaces/data-api/beta/utils.ts | 81 ++- 10 files changed, 1565 insertions(+), 764 deletions(-) diff --git a/src/interfaces/data-api/beta/components.ts b/src/interfaces/data-api/beta/components.ts index e1f4806..6a2f874 100644 --- a/src/interfaces/data-api/beta/components.ts +++ b/src/interfaces/data-api/beta/components.ts @@ -1,26 +1,45 @@ -import { MakeParameterAndPropertyDecorator } from '@ajs/core/beta/decorators'; -import { RequestContext, SetParameterProvider } from '@ajs/api/beta'; -import { Datum, Stream, Table, ValueProxy, SchemaInstance } from '@ajs/database/beta'; -import { DataModel } from '@ajs/database-decorators/beta/model'; -import { assert } from '@ajs/api-util/beta'; -import { DataAPIMeta, FilterValue } from './metadata'; -import { GetDataControllerMeta } from '.'; -import { fromDatabase, lock, toPlainData, unlock, unlockrequest } from '@ajs/database-decorators/beta/modifiers/common'; -import { Constructible } from '@ajs/database-decorators/beta/common'; +import { type RequestContext, SetParameterProvider } from "@ajs/api/beta"; +import { assert } from "@ajs/api-util/beta"; +import { MakeParameterAndPropertyDecorator } from "@ajs/core/beta/decorators"; +import { + type Datum, + type SchemaInstance, + Stream, + type Table, + type ValueProxy, +} from "@ajs/database/beta"; +import type { Constructible } from "@ajs/database-decorators/beta/common"; +import type { DataModel } from "@ajs/database-decorators/beta/model"; +import { + fromDatabase, + lock, + toPlainData, + unlock, + unlockrequest, +} from "@ajs/database-decorators/beta/modifiers/common"; +import { GetDataControllerMeta } from "."; +import type { DataAPIMeta, FilterValue } from "./metadata"; export namespace Parameters { - export function GetOptionOverrides>(reqCtx: RequestContext): T { + export function GetOptionOverrides>( + reqCtx: RequestContext, + ): T { return (reqCtx).dataAPIEntry?.options ?? {}; } - export function ExtractFilters(reqCtx: RequestContext, meta: DataAPIMeta): Record { + export function ExtractFilters( + reqCtx: RequestContext, + meta: DataAPIMeta, + ): Record { const result: Record = {}; for (const filter of Object.keys(meta.filters)) { const filter_key = `filter_${filter}`; if (reqCtx.url.searchParams.has(filter_key)) { - const searchVal = reqCtx.url.searchParams.get(filter_key)!; + const searchVal = reqCtx.url.searchParams.get(filter_key) ?? ""; const match = searchVal.match(/([^:]+):(.*)/); - result[filter] = match ? [match[2], match[1] as FilterValue[1]] : [searchVal, 'eq']; + result[filter] = match + ? [match[2], match[1] as FilterValue[1]] + : [searchVal, "eq"]; } } return result; @@ -28,13 +47,16 @@ export namespace Parameters { const converters = { number: (val: string) => parseFloat(val), - int: (val: string) => parseInt(val), - bool: (val: string) => (val === '0' ? false : true), + int: (val: string) => parseInt(val, 10), + bool: (val: string) => val !== "0", string: (val: string) => val, }; type ConvertersKey = keyof typeof converters; type GenericParams> = { - [K in keyof T]: ConvertersKey | `multi:${ConvertersKey}` | ((reqCtx: RequestContext, meta: DataAPIMeta) => any); + [K in keyof T]: + | ConvertersKey + | `multi:${ConvertersKey}` + | ((reqCtx: RequestContext, meta: DataAPIMeta) => any); }; export function ExtractGeneric>( reqCtx: RequestContext, @@ -46,8 +68,8 @@ export namespace Parameters { for (const key of Object.keys(dynamic)) { if (!(key in result)) { const extractor = dynamic[key]; - if (typeof extractor === 'string') { - if (extractor.startsWith('multi:')) { + if (typeof extractor === "string") { + if (extractor.startsWith("multi:")) { const converter = converters[extractor.substring(6)]; result[key as keyof T] = reqCtx.url.searchParams .getAll(key) @@ -55,7 +77,9 @@ export namespace Parameters { } else { const searchVal = reqCtx.url.searchParams.get(key); if (searchVal !== null) { - result[key as keyof T] = converters[extractor](searchVal) as any; + result[key as keyof T] = converters[extractor]( + searchVal, + ) as any; } } } else { @@ -73,7 +97,7 @@ export namespace Parameters { limit?: number; sortKey?: string; - sortDirection?: 'asc' | 'desc'; + sortDirection?: "asc" | "desc"; maxPage?: number; noForeign?: boolean; @@ -87,14 +111,20 @@ export namespace Parameters { const meta = GetDataControllerMeta(this); const params = ExtractGeneric(context, meta, { filters: ExtractFilters, - offset: 'int', - limit: 'int', - sortKey: 'string', - sortDirection: 'string', + offset: "int", + limit: "int", + sortKey: "string", + sortDirection: "string", }); - assert(!params.sortKey || meta.fields[params.sortKey]?.sortable, 400, 'Field is not sortable.'); - params.limit = params.limit ? Math.min(params.limit, params.maxPage ?? 100) : params.maxPage; + assert( + !params.sortKey || meta.fields[params.sortKey]?.sortable, + 400, + "Field is not sortable.", + ); + params.limit = params.limit + ? Math.min(params.limit, params.maxPage ?? 100) + : params.maxPage; return params; }), @@ -108,11 +138,15 @@ export namespace Parameters { export const Get = MakeParameterAndPropertyDecorator((target, key, param) => SetParameterProvider(target, key, param, function (this: unknown, context) { - const params = ExtractGeneric(context, GetDataControllerMeta(this), { - id: 'string', - }); + const params = ExtractGeneric( + context, + GetDataControllerMeta(this), + { + id: "string", + }, + ); - assert(params.id && typeof params.id === 'string', 400, 'Missing id.'); + assert(params.id && typeof params.id === "string", 400, "Missing id."); return params; }), @@ -124,7 +158,11 @@ export namespace Parameters { export const New = MakeParameterAndPropertyDecorator((target, key, param) => SetParameterProvider(target, key, param, function (this: unknown, context) { - const params = ExtractGeneric(context, GetDataControllerMeta(this), {}); + const params = ExtractGeneric( + context, + GetDataControllerMeta(this), + {}, + ); return params; }), @@ -138,11 +176,15 @@ export namespace Parameters { export const Edit = MakeParameterAndPropertyDecorator((target, key, param) => SetParameterProvider(target, key, param, function (this: unknown, context) { - const params = ExtractGeneric(context, GetDataControllerMeta(this), { - id: 'string', - }); + const params = ExtractGeneric( + context, + GetDataControllerMeta(this), + { + id: "string", + }, + ); - assert(params.id && typeof params.id === 'string', 400, 'Missing id.'); + assert(params.id && typeof params.id === "string", 400, "Missing id."); return params; }), @@ -152,22 +194,39 @@ export namespace Parameters { id: string[]; } - export const Delete = MakeParameterAndPropertyDecorator((target, key, param) => - SetParameterProvider(target, key, param, function (this: unknown, context) { - const params = ExtractGeneric(context, GetDataControllerMeta(this), { - id: 'multi:string', - }); - - assert(params.id && Array.isArray(params.id) && params.id.length > 0, 400, 'Missing id.'); - - return params; - }), + export const Delete = MakeParameterAndPropertyDecorator( + (target, key, param) => + SetParameterProvider( + target, + key, + param, + function (this: unknown, context) { + const params = ExtractGeneric( + context, + GetDataControllerMeta(this), + { + id: "multi:string", + }, + ); + + assert( + params.id && Array.isArray(params.id) && params.id.length > 0, + 400, + "Missing id.", + ); + + return params; + }, + ), ); } export namespace Query { - export function GetModel(obj: any, meta: DataAPIMeta): InstanceType & { constructor: DataModel } { - assert(meta.modelKey, 500, 'Missing model key.'); + export function GetModel( + obj: any, + meta: DataAPIMeta, + ): InstanceType & { constructor: DataModel } { + assert(meta.modelKey, 500, "Missing model key."); return obj[meta.modelKey]; } @@ -196,11 +255,15 @@ export namespace Query { continue; } const [table, _tableClass, index, _multi, pluckField] = field.foreign; - let other = db.table(table); + const other = db.table(table); if (pluckField) { - query = query.lookup(other.pluck('_internal', ...pluckField) as Table, name, index || '_id'); + query = query.lookup( + other.pluck("_internal", ...pluckField) as Table, + name, + index || "_id", + ); } else { - query = query.lookup(other, name, index || '_id'); + query = query.lookup(other, name, index || "_id"); } } return query; @@ -213,15 +276,28 @@ export namespace Query { } const [table, _tableClass, index, multi, pluckField] = field.foreign; if (multi) { - changedFields[name] = (obj.key(name) as ValueProxy).default([]).map((val) => { - let foreignObject: Datum = Get(db.table(table), val, index); - if (pluckField) { - foreignObject = foreignObject.pluck('_internal', ...pluckField); - } - return foreignObject.default(null); - }); + changedFields[name] = (obj.key(name) as ValueProxy) + .default([]) + .map((val) => { + let foreignObject: Datum = Get( + db.table(table), + val, + index, + ); + if (pluckField) { + foreignObject = foreignObject.pluck( + "_internal", + ...pluckField, + ); + } + return foreignObject.default(null); + }); } else { - changedFields[name] = Get(db.table(table), obj.key(name) as ValueProxy, index).default(null); + changedFields[name] = Get( + db.table(table), + obj.key(name) as ValueProxy, + index, + ).default(null); } } return obj.merge(changedFields); @@ -230,8 +306,15 @@ export namespace Query { } } - export async function ReadProperties(obj: any, meta: DataAPIMeta, dbData: any, action?: string, onlyList?: boolean) { - const readable = meta.readable[action ?? '_default'] ?? meta.readable['_default']; + export async function ReadProperties( + obj: any, + meta: DataAPIMeta, + dbData: any, + action?: string, + onlyList?: boolean, + ) { + const readable = + meta.readable[action ?? "_default"] ?? meta.readable._default; const instance: Record = { ...obj }; const res: Record = {}; for (const [key, field] of readable.props) { @@ -248,7 +331,7 @@ export namespace Query { continue; } const val = field.desc?.get?.apply(instance); - res[key] = await (typeof val === 'function' ? val() : val); + res[key] = await (typeof val === "function" ? val() : val); } return res; } @@ -260,7 +343,8 @@ export namespace Query { action?: string, existingDBData?: Record, ) { - const writable = meta.writable[action ?? '_default'] ?? meta.writable['_default']; + const writable = + meta.writable[action ?? "_default"] ?? meta.writable._default; const instance: Record = { ...obj }; const dbData: Record = existingDBData || {}; Object.setPrototypeOf(dbData, meta.tableClass.prototype); @@ -286,8 +370,14 @@ export namespace Query { return dbData; } - export function Get(table: Table, id: string | ValueProxy, index?: string) { - return index ? table.getAll(id as string, index).nth(0) : table.get(id as string); + export function Get( + table: Table, + id: string | ValueProxy, + index?: string, + ) { + return index + ? table.getAll(id as string, index).nth(0) + : table.get(id as string); } export function List>( @@ -295,46 +385,58 @@ export namespace Query { meta: DataAPIMeta, request: Table, reqCtx: RequestContext, - sorting?: [string, 'asc' | 'desc' | undefined], + sorting?: [string, "asc" | "desc" | undefined], filters?: Record, ): [sorted: Stream, total: Datum] { - const filterList = Object.entries(meta.filters).filter(([name]) => filters && name in filters); - const indexFilter = filterList.find(([name]) => filters![name][1] === 'eq' && meta.fields[name]?.indexable)?.[0]; - const index = indexFilter ? meta.fields[indexFilter].dbName || indexFilter : undefined; - - let tmpRequest = index ? request.getAll(filters![indexFilter!][0], index) : request; - - const shouldSort = sorting && meta.fields[sorting[0]]?.sortable; - if (shouldSort && shouldSort.indexed) { - tmpRequest = tmpRequest.orderBy(sorting[0], sorting[1] ?? 'asc'); + const filterList = Object.entries(meta.filters).filter( + ([name]) => filters && name in filters, + ); + const indexFilter = filterList.find( + ([name]) => filters?.[name][1] === "eq" && meta.fields[name]?.indexable, + )?.[0]; + const index = indexFilter + ? meta.fields[indexFilter].dbName || indexFilter + : undefined; + const indexedFilter = indexFilter ? filters?.[indexFilter] : undefined; + + let tmpRequest = index + ? request.getAll(indexedFilter?.[0] ?? "", index) + : request; + + const sortField = sorting?.[0]; + const shouldSort = sortField ? meta.fields[sortField]?.sortable : undefined; + if (shouldSort?.indexed && sortField) { + tmpRequest = tmpRequest.orderBy(sortField, sorting?.[1] ?? "asc"); } if (filterList.length > 0) { // TODO: rework for modifier-affected fields (unlockrequest) - tmpRequest = filterList.reduce( - (req, [name, filter]) => - name === indexFilter - ? req - : req.filter((row) => - filter( - Object.assign(reqCtx, { this: obj }), - Validation.UnlockRequest(obj, meta, row, name), - name, - filters![name][0], - filters![name][1], - row as ValueProxy>, - ), + tmpRequest = filterList.reduce((req, [name, filter]) => { + const filterValue = filters?.[name]; + if (!filterValue) return req; + return name === indexFilter + ? req + : req.filter((row) => + filter( + Object.assign(reqCtx, { this: obj }), + Validation.UnlockRequest(obj, meta, row, name), + name, + filterValue[0], + filterValue[1], + row as ValueProxy>, ), - tmpRequest, - ); + ); + }, tmpRequest); } - if (shouldSort && !shouldSort.indexed) { - tmpRequest = tmpRequest.orderBy(sorting[0], sorting[1] ?? 'asc'); + if (shouldSort && !shouldSort.indexed && sortField) { + tmpRequest = tmpRequest.orderBy(sortField, sorting?.[1] ?? "asc"); } return [tmpRequest, tmpRequest.count()]; } export function Delete(table: Table, id: string | string[]) { - return Array.isArray(id) ? table.getAll(id).delete() : table.get(id).delete(); + return Array.isArray(id) + ? table.getAll(id).delete() + : table.get(id).delete(); } } @@ -343,17 +445,32 @@ export namespace Validation { const missing = Object.entries(meta.fields) .filter(([name, field]) => field.mandatory?.has(type) && !(name in obj)) .map(([name]) => name); - assert(missing.length === 0, 400, `Missing mandatory fields: ${missing.join(', ')}`); + assert( + missing.length === 0, + 400, + `Missing mandatory fields: ${missing.join(", ")}`, + ); } - export async function ValidateTypes(meta: DataAPIMeta, obj: Record) { + export async function ValidateTypes( + meta: DataAPIMeta, + obj: Record, + ) { const invalid: string[] = []; for (const [name, field] of Object.entries(meta.fields)) { - if (field.validator && name in obj && !(await field.validator(obj[name]))) { + if ( + field.validator && + name in obj && + !(await field.validator(obj[name])) + ) { invalid.push(name); } } - assert(invalid.length === 0, 400, `Invalid field type(s): ${invalid.join(', ')}`); + assert( + invalid.length === 0, + 400, + `Invalid field type(s): ${invalid.join(", ")}`, + ); } export function Lock(obj: any, meta: DataAPIMeta, data: any) { @@ -365,9 +482,15 @@ export namespace Validation { export function Unlock(obj: any, meta: DataAPIMeta, dbData: any) { for (const [name, field] of Object.entries(meta.fields)) { - if (field.foreign && dbData[name] && typeof dbData[name] === 'object' && field.foreign[1]) { + if ( + field.foreign && + dbData[name] && + typeof dbData[name] === "object" && + field.foreign[1] + ) { + const foreignSchema = field.foreign[1]; dbData[name] = Array.isArray(dbData[name]) - ? dbData[name].map((entry) => fromDatabase(entry, field.foreign![1]!)) + ? dbData[name].map((entry) => fromDatabase(entry, foreignSchema)) : fromDatabase(dbData[name], field.foreign[1]); } } @@ -375,7 +498,7 @@ export namespace Validation { const key = obj[field]; unlock(dbData, modifier, undefined, key); for (const [foreign, fieldData] of Object.entries(meta.fields)) { - if (fieldData.foreign && typeof dbData[foreign] === 'object') { + if (fieldData.foreign && typeof dbData[foreign] === "object") { unlock(dbData[foreign], modifier, undefined, key); } } @@ -388,25 +511,37 @@ export namespace Validation { row: ValueProxy, field: K, ): ValueProxy { - const modifiers = Array.from(meta.modifierKeys.entries()).map(([modifier, field]) => ({ - modifier, - args: [obj[field]], - })); - return unlockrequest(meta.tableClass as Constructible, row, field, modifiers); + const modifiers = Array.from(meta.modifierKeys.entries()).map( + ([modifier, field]) => ({ + modifier, + args: [obj[field]], + }), + ); + return unlockrequest( + meta.tableClass as Constructible, + row, + field, + modifiers, + ); } - export function ClearInternal(meta: DataAPIMeta, obj: Record | Array>) { + export function ClearInternal( + meta: DataAPIMeta, + obj: Record | Array>, + ) { const results = Array.isArray(obj) ? obj : [obj]; - const foreignFields = Object.entries(meta.fields).filter(([, field]) => field.foreign); + const foreignFields = Object.entries(meta.fields).filter( + ([, field]) => field.foreign, + ); for (const entry of results) { delete entry._internal; for (const [name, { foreign }] of foreignFields) { if ( foreign && entry[name] && - typeof entry[name] === 'object' && - (!Array.isArray(entry[name]) || typeof entry[name][0] === 'object') + typeof entry[name] === "object" && + (!Array.isArray(entry[name]) || typeof entry[name][0] === "object") ) { const processForeign = (data: any) => { const plain = toPlainData(data); @@ -419,7 +554,9 @@ export namespace Validation { } return plainPlucked; }; - entry[name] = Array.isArray(entry[name]) ? entry[name].map(processForeign) : processForeign(entry[name]); + entry[name] = Array.isArray(entry[name]) + ? entry[name].map(processForeign) + : processForeign(entry[name]); } } } diff --git a/src/interfaces/data-api/beta/metadata.ts b/src/interfaces/data-api/beta/metadata.ts index dfc37d9..bd7f442 100644 --- a/src/interfaces/data-api/beta/metadata.ts +++ b/src/interfaces/data-api/beta/metadata.ts @@ -1,10 +1,19 @@ -import { GetMetadata } from '@ajs/core/beta'; -import { Class, MakeMethodAndPropertyDecorator, MakePropertyDecorator } from '@ajs/core/beta/decorators'; -import { RequestContext } from '@ajs/api/beta'; -import { ValueProxy, ValueProxyOrValue } from '@ajs/database/beta'; -import { DataControllerCallbackWithOptions } from '.'; -import { ContainerModifier } from '@ajs/database-decorators/beta/modifiers/common'; -import { getTablesForSchema, Table, DatumStaticMetadata, getMetadata } from '@ajs/database-decorators/beta'; +import type { RequestContext } from "@ajs/api/beta"; +import { GetMetadata } from "@ajs/core/beta"; +import { + type Class, + MakeMethodAndPropertyDecorator, + MakePropertyDecorator, +} from "@ajs/core/beta/decorators"; +import type { ValueProxy, ValueProxyOrValue } from "@ajs/database/beta"; +import { + DatumStaticMetadata, + getMetadata, + getTablesForSchema, + type Table, +} from "@ajs/database-decorators/beta"; +import type { ContainerModifier } from "@ajs/database-decorators/beta/modifiers/common"; +import type { DataControllerCallbackWithOptions } from "."; /** * Field access mode enum. @@ -54,7 +63,13 @@ export interface FieldData { /** * Foreign key reference. */ - foreign?: [table: string, tableClass?: Class, index?: string, multi?: true, pluck?: string[]]; + foreign?: [ + table: string, + tableClass?: Class
, + index?: string, + multi?: true, + pluck?: string[], + ]; /** * Value validator callback. @@ -72,13 +87,16 @@ export interface FieldData { indexable?: boolean; } -type Comparison = 'eq' | 'ne' | 'gt' | 'ge' | 'lt' | 'le'; +type Comparison = "eq" | "ne" | "gt" | "ge" | "lt" | "le"; export type FilterValue = [value: string, mode: Comparison]; /** * Filter callback. */ -export type FilterFunction, U extends Record = Record> = ( +export type FilterFunction< + T extends Record, + U extends Record = Record, +> = ( context: RequestContext & { this: T }, proxy: ValueProxy, key: string, @@ -156,7 +174,8 @@ export class DataAPIMeta { /** * Registered DataAPI endpoints. */ - public readonly endpoints: Record = {}; + public readonly endpoints: Record = + {}; constructor(public readonly target: Class) {} @@ -175,12 +194,13 @@ export class DataAPIMeta { for (const [key, list] of Object.entries(parent.pluck)) { this.pluck[key] = new Set(list); } - if (!('modelKey' in this)) { + if (!("modelKey" in this)) { this.modelKey = parent.modelKey; } for (const key of parent.modifierKeys.keys()) { if (!this.modifierKeys.has(key)) { - this.modifierKeys.set(key, parent.modifierKeys.get(key)!); + const value = parent.modifierKeys.get(key); + if (value !== undefined) this.modifierKeys.set(key, value); } } this.recomputeAccess(); @@ -198,14 +218,18 @@ export class DataAPIMeta { } private recomputeListable() { - Object.values(this.pluck).forEach((set) => set.clear()); + for (const set of Object.values(this.pluck)) { + set.clear(); + } for (const [_, field] of Object.entries(this.fields)) { if (field.listable) { for (const [mode, names] of Object.entries(field.listable)) { if (!(mode in this.pluck)) { this.pluck[mode] = new Set(); } - names.forEach((name) => this.pluck[mode].add(name)); + for (const name of names) { + this.pluck[mode].add(name); + } } } } @@ -219,7 +243,7 @@ export class DataAPIMeta { delete this.writable[key]; } - const actions = new Set(['_default']); + const actions = new Set(["_default"]); for (const field of Object.values(this.fields)) { if (field.modeOverrides) { for (const action of Object.keys(field.modeOverrides)) { @@ -235,17 +259,20 @@ export class DataAPIMeta { for (const [key, field] of Object.entries(this.fields)) { for (const action of actions) { - const effectiveMode = action === '_default' ? field.mode : (field.modeOverrides?.[action] ?? field.mode); + const effectiveMode = + action === "_default" + ? field.mode + : (field.modeOverrides?.[action] ?? field.mode); if (!effectiveMode) { continue; } if (effectiveMode & AccessMode.ReadOnly) { - const target = field.desc?.get ? 'getters' : 'props'; + const target = field.desc?.get ? "getters" : "props"; this.readable[action][target].push([key, field]); } if (effectiveMode & AccessMode.WriteOnly) { - const target = field.desc?.set ? 'setters' : 'props'; + const target = field.desc?.set ? "setters" : "props"; this.writable[action][target].push([key, field]); } } @@ -258,7 +285,11 @@ export class DataAPIMeta { * @param name Field name * @param mode Access mode */ - public setMode(name: string, mode: AccessMode, overrides?: Record) { + public setMode( + name: string, + mode: AccessMode, + overrides?: Record, + ) { const field = this.field(name); field.mode = mode; field.modeOverrides = overrides; @@ -273,9 +304,18 @@ export class DataAPIMeta { * @param requiredFields Boolean or table field list * @param mode List mode (default: 'list') */ - public setListable(name: string, requiredFields: boolean | string[], mode = 'list') { + public setListable( + name: string, + requiredFields: boolean | string[], + mode = "list", + ) { const field = this.field(name); - const names = typeof requiredFields === 'boolean' ? (requiredFields ? [name] : []) : requiredFields; + const names = + typeof requiredFields === "boolean" + ? requiredFields + ? [name] + : [] + : requiredFields; if (!field.listable) { field.listable = {}; } @@ -316,14 +356,41 @@ export class DataAPIMeta { * @param index Other table index * @param multi Index is a multi index */ - public setForeign(name: string, table: string | Class
, index?: string, multi?: boolean, pluck?: string[]) { - const databaseSchema = getTablesForSchema(this.schemaName)!; - if (typeof table === 'string') { - this.field(name).foreign = [table, databaseSchema[table], index, multi || undefined, pluck || undefined]; + public setForeign( + name: string, + table: string | Class
, + index?: string, + multi?: boolean, + pluck?: string[], + ) { + const databaseSchema = getTablesForSchema(this.schemaName); + if (!databaseSchema) + throw new Error(`Schema "${this.schemaName}" not found`); + if (typeof table === "string") { + this.field(name).foreign = [ + table, + databaseSchema[table], + index, + multi || undefined, + pluck || undefined, + ]; } else { - const tableName = Object.entries(databaseSchema).find(([, table_]) => table_ === table)![0]; + const tableName = Object.entries(databaseSchema).find( + ([, table_]) => table_ === table, + )?.[0]; + if (!tableName) { + throw new Error( + `Unable to infer foreign table name for field "${name}"`, + ); + } - this.field(name).foreign = [tableName, table, index, multi || undefined, pluck || undefined]; + this.field(name).foreign = [ + tableName, + table, + index, + multi || undefined, + pluck || undefined, + ]; } return this; } @@ -334,7 +401,10 @@ export class DataAPIMeta { * @param name Field name * @param validator Value validator callback */ - public setValidator(name: string, validator?: (value: unknown) => boolean | Promise) { + public setValidator( + name: string, + validator?: (value: unknown) => boolean | Promise, + ) { this.field(name).validator = validator; return this; } @@ -357,7 +427,11 @@ export class DataAPIMeta { * @param func Filter callback * @param index */ - public setFilter(name: string, func: FilterFunction, Record>, useIndex?: boolean) { + public setFilter( + name: string, + func: FilterFunction, Record>, + useIndex?: boolean, + ) { this.filters[name] = func; const field = this.field(name); if (useIndex) { @@ -388,7 +462,10 @@ export class DataAPIMeta { * @param name Field name * @param modifierClass Modifier */ - public setModifierKey(name: string, modifierClass: typeof ContainerModifier) { + public setModifierKey( + name: string, + modifierClass: typeof ContainerModifier, + ) { this.modifierKeys.set(modifierClass, name); return this; } @@ -399,7 +476,10 @@ export class DataAPIMeta { * @param key field name * @param endpoint callback information */ - public addEndpoint(key: string, endpoint?: DataControllerCallbackWithOptions) { + public addEndpoint( + key: string, + endpoint?: DataControllerCallbackWithOptions, + ) { if (!endpoint) { delete this.endpoints[key]; } else { @@ -414,7 +494,13 @@ export class DataAPIMeta { * @param mode Access mode */ export const Access = MakeMethodAndPropertyDecorator( - (target, key, desc, mode: AccessMode, overrides?: Record) => { + ( + target, + key, + desc, + mode: AccessMode, + overrides?: Record, + ) => { GetMetadata(target.constructor, DataAPIMeta) .setDescriptor(key as string, desc) .setMode(key as string, mode, overrides); @@ -434,7 +520,11 @@ export const Listable = MakeMethodAndPropertyDecorator( (target, key, desc, requiredFields?: boolean | string[], mode?: string) => { GetMetadata(target.constructor, DataAPIMeta) .setDescriptor(key as string, desc) - .setListable(key as string, typeof requiredFields !== 'undefined' ? requiredFields : true, mode); + .setListable( + key as string, + typeof requiredFields !== "undefined" ? requiredFields : true, + mode, + ); }, ); @@ -443,11 +533,13 @@ export const Listable = MakeMethodAndPropertyDecorator( * * @param modes DataAPI methods (ex: `new`, `edit`) */ -export const Mandatory = MakeMethodAndPropertyDecorator((target, key, desc, ...modes: string[]) => { - GetMetadata(target.constructor, DataAPIMeta) - .setDescriptor(key as string, desc) - .setMandatory(key as string, modes); -}); +export const Mandatory = MakeMethodAndPropertyDecorator( + (target, key, desc, ...modes: string[]) => { + GetMetadata(target.constructor, DataAPIMeta) + .setDescriptor(key as string, desc) + .setMandatory(key as string, modes); + }, +); /** * Declares a field as being optional. @@ -465,11 +557,13 @@ export const Optional = MakeMethodAndPropertyDecorator((target, key, desc) => { * * @param options Options */ -export const Sortable = MakeMethodAndPropertyDecorator((target, key, desc, options?: { noIndex?: boolean }) => { - GetMetadata(target.constructor, DataAPIMeta) - .setDescriptor(key as string, desc) - .setSortable(key as string, true, options?.noIndex ?? false); -}); +export const Sortable = MakeMethodAndPropertyDecorator( + (target, key, desc, options?: { noIndex?: boolean }) => { + GetMetadata(target.constructor, DataAPIMeta) + .setDescriptor(key as string, desc) + .setSortable(key as string, true, options?.noIndex ?? false); + }, +); /** * Declares a field to be a foreign key. @@ -479,7 +573,15 @@ export const Sortable = MakeMethodAndPropertyDecorator((target, key, desc, optio * @param multi Index is a multi index */ export const Foreign = MakeMethodAndPropertyDecorator( - (target, key, desc, table: string | Class
, index?: string, multi?: boolean, pluck?: string[]) => { + ( + target, + key, + desc, + table: string | Class
, + index?: string, + multi?: boolean, + pluck?: string[], + ) => { GetMetadata(target.constructor, DataAPIMeta) .setDescriptor(key as string, desc) .setForeign(key as string, table, index, multi, pluck); @@ -492,16 +594,27 @@ export const Foreign = MakeMethodAndPropertyDecorator( * @param validator Value validator callback */ export const Validator = MakeMethodAndPropertyDecorator( - (target, key, desc, validator: (val: unknown) => boolean | Promise) => { + ( + target, + key, + desc, + validator: (val: unknown) => boolean | Promise, + ) => { GetMetadata(target.constructor, DataAPIMeta) .setDescriptor(key as string, desc) .setValidator(key as string, validator); }, ); -type ProxyFilterOperator = (proxy: ValueProxy, value: string) => ValueProxyOrValue; +type ProxyFilterOperator = ( + proxy: ValueProxy, + value: string, +) => ValueProxyOrValue; type DefaultFilterOperators = Record; -type DefaultFilterFunction = FilterFunction, Record>; +type DefaultFilterFunction = FilterFunction< + Record, + Record +>; const DEFAULT_FILTER_OPERATORS: DefaultFilterOperators = { eq: (proxy, value) => proxy.eq(value), @@ -521,7 +634,8 @@ function applyDefaultFilterMode( } function createDefaultFilter(): DefaultFilterFunction { - return (_context, proxy, _key, value, mode) => applyDefaultFilterMode(proxy as ValueProxy, value, mode); + return (_context, proxy, _key, value, mode) => + applyDefaultFilterMode(proxy as ValueProxy, value, mode); } const FilterDecoratorFactory = MakePropertyDecorator( @@ -556,6 +670,11 @@ export const ModelReference = MakePropertyDecorator((target, key) => { * * @param modifierClass Modifier */ -export const ModifierKey = MakePropertyDecorator((target, key, modifierClass: typeof ContainerModifier) => { - GetMetadata(target.constructor, DataAPIMeta).setModifierKey(key as string, modifierClass); -}); +export const ModifierKey = MakePropertyDecorator( + (target, key, modifierClass: typeof ContainerModifier) => { + GetMetadata(target.constructor, DataAPIMeta).setModifierKey( + key as string, + modifierClass, + ); + }, +); diff --git a/src/test/interfaces/data-api/beta/components/access_control.test.ts b/src/test/interfaces/data-api/beta/components/access_control.test.ts index 68d3c4c..f373164 100644 --- a/src/test/interfaces/data-api/beta/components/access_control.test.ts +++ b/src/test/interfaces/data-api/beta/components/access_control.test.ts @@ -1,22 +1,37 @@ -import { expect } from 'chai'; -import { Schema, SchemaInstance } from '@ajs/database/beta'; +import path from "node:path"; +import { Controller } from "@ajs/api/beta"; +import type { SchemaInstance } from "@ajs/database/beta"; import { - Table, - Index, - RegisterTable, - CreateDatabaseSchemaInstance, BasicDataModel, + CreateDatabaseSchemaInstance, + Index, Model, -} from '@ajs/database-decorators/beta'; -import { Controller } from '@ajs/api/beta'; -import { DataController, DefaultRoutes, RegisterDataController } from '@ajs.local/data-api/beta'; -import { Access, AccessMode, ModelReference } from '@ajs.local/data-api/beta/metadata'; -import { editRequest, getFunctionName, getRequest } from '../utils'; -import path from 'node:path'; - -const currentTestName = path.basename(__filename).replace(/\.test\.(ts|js)$/, ''); + RegisterTable, + Table, +} from "@ajs/database-decorators/beta"; +import { + DataController, + DefaultRoutes, + RegisterDataController, +} from "@ajs.local/data-api/beta"; +import { + Access, + AccessMode, + ModelReference, +} from "@ajs.local/data-api/beta/metadata"; +import { expect } from "chai"; +import { + editRequest, + getFunctionName, + getRequest, + getSchemaInstance, +} from "../utils"; + +const currentTestName = path + .basename(__filename) + .replace(/\.test\.(ts|js)$/, ""); const userTableName = `users-${currentTestName}`; -const schemaName = 'default'; +const schemaName = "default"; @RegisterTable(userTableName, schemaName) class User extends Table { @@ -33,26 +48,30 @@ class UserModel extends BasicDataModel(User, userTableName) {} let database: SchemaInstance; const defaultUserDataset: Partial = { - name: 'Jean Test', - email: 'jean.test@email.com', + name: "Jean Test", + email: "jean.test@email.com", age: 30, - password: 'very-secure-qwerty123', + password: "very-secure-qwerty123", }; -describe('Field Access Control', () => { - it('read in a read write field', async () => await readInReadWriteField()); - it('write in a read write field', async () => await writeInReadWriteField()); - it('read in a write only field', async () => await readInWriteOnlyField()); - it('write in a read only field', async () => await writeInReadOnlyField()); - it('read in a read only field', async () => await readInReadOnlyField()); - it('write in a write only field', async () => await writeInWriteOnlyField()); +describe("Field Access Control", () => { + it("read in a read write field", async () => await readInReadWriteField()); + it("write in a read write field", async () => await writeInReadWriteField()); + it("read in a write only field", async () => await readInWriteOnlyField()); + it("write in a read only field", async () => await writeInReadOnlyField()); + it("read in a read only field", async () => await readInReadOnlyField()); + it("write in a write only field", async () => await writeInWriteOnlyField()); after(async () => {}); }); async function _createDataController(testName: string, user: Partial) { @RegisterDataController() - class _AccessTestAPI extends DataController(User, DefaultRoutes.All, Controller(`/${testName}`)) { + class _AccessTestAPI extends DataController( + User, + DefaultRoutes.All, + Controller(`/${testName}`), + ) { @ModelReference() @Model(UserModel) declare userModel: UserModel; @@ -73,14 +92,17 @@ async function _createDataController(testName: string, user: Partial) { declare email: string; } await CreateDatabaseSchemaInstance(schemaName); - database = Schema.get(schemaName)!.instance(); + database = getSchemaInstance(schemaName); const userModel = new UserModel(database); const insertResult = await userModel.insert(user); return { id: insertResult[0], userModel }; } async function readInReadWriteField() { - const { id } = await _createDataController(getFunctionName(), defaultUserDataset); + const { id } = await _createDataController( + getFunctionName(), + defaultUserDataset, + ); const response = await getRequest(getFunctionName(), { id }); expect(response.status).to.equal(200); @@ -92,17 +114,27 @@ async function readInReadWriteField() { } async function writeInReadWriteField() { - const { id, userModel } = await _createDataController(getFunctionName(), defaultUserDataset); - - const replacementEmail = 'bob.test@email.com'; - const response = await editRequest(getFunctionName(), { email: replacementEmail }, { id }); + const { id, userModel } = await _createDataController( + getFunctionName(), + defaultUserDataset, + ); + + const replacementEmail = "bob.test@email.com"; + const response = await editRequest( + getFunctionName(), + { email: replacementEmail }, + { id }, + ); expect(response.status).to.equal(200); const user = await userModel.get(id); expect(user?.email).to.equal(replacementEmail); } async function readInWriteOnlyField() { - const { id } = await _createDataController(getFunctionName(), defaultUserDataset); + const { id } = await _createDataController( + getFunctionName(), + defaultUserDataset, + ); const response = await getRequest(getFunctionName(), { id }); expect(response.status).to.equal(200); @@ -111,16 +143,26 @@ async function readInWriteOnlyField() { } async function writeInReadOnlyField() { - const { id, userModel } = await _createDataController(getFunctionName(), defaultUserDataset); - - const response = await editRequest(getFunctionName(), { age: (defaultUserDataset.age ?? 0) + 5 }, { id }); + const { id, userModel } = await _createDataController( + getFunctionName(), + defaultUserDataset, + ); + + const response = await editRequest( + getFunctionName(), + { age: (defaultUserDataset.age ?? 0) + 5 }, + { id }, + ); expect(response.status).to.equal(200); const user = await userModel.get(id); expect(user?.age).to.equal(defaultUserDataset.age); } async function readInReadOnlyField() { - const { id } = await _createDataController(getFunctionName(), defaultUserDataset); + const { id } = await _createDataController( + getFunctionName(), + defaultUserDataset, + ); const response = await getRequest(getFunctionName(), { id }); expect(response.status).to.equal(200); @@ -129,10 +171,17 @@ async function readInReadOnlyField() { } async function writeInWriteOnlyField() { - const { id, userModel } = await _createDataController(getFunctionName(), defaultUserDataset); - - const replacementPassword = 'new-password'; - const response = await editRequest(getFunctionName(), { password: replacementPassword }, { id }); + const { id, userModel } = await _createDataController( + getFunctionName(), + defaultUserDataset, + ); + + const replacementPassword = "new-password"; + const response = await editRequest( + getFunctionName(), + { password: replacementPassword }, + { id }, + ); expect(response.status).to.equal(200); const user = await userModel.get(id); expect(user?.password).to.equal(replacementPassword); diff --git a/src/test/interfaces/data-api/beta/components/listable.test.ts b/src/test/interfaces/data-api/beta/components/listable.test.ts index a0817b9..585e450 100644 --- a/src/test/interfaces/data-api/beta/components/listable.test.ts +++ b/src/test/interfaces/data-api/beta/components/listable.test.ts @@ -1,22 +1,40 @@ -import { expect } from 'chai'; -import { Schema } from '@ajs/database/beta'; +import path from "node:path"; +import { Controller } from "@ajs/api/beta"; +import { Schema } from "@ajs/database/beta"; import { - Table, - Index, - RegisterTable, - CreateDatabaseSchemaInstance, BasicDataModel, + CreateDatabaseSchemaInstance, + Index, Model, -} from '@ajs/database-decorators/beta'; -import { Controller } from '@ajs/api/beta'; -import { DataController, DefaultRoutes, RegisterDataController } from '@ajs.local/data-api/beta'; -import { Access, AccessMode, Listable, ModelReference, Sortable } from '@ajs.local/data-api/beta/metadata'; -import { getFunctionName, listRequest, request, validateObjectList } from '../utils'; -import path from 'node:path'; - -const currentTestName = path.basename(__filename).replace(/\.test\.(ts|js)$/, ''); + RegisterTable, + Table, +} from "@ajs/database-decorators/beta"; +import { + DataController, + DefaultRoutes, + RegisterDataController, +} from "@ajs.local/data-api/beta"; +import { + Access, + AccessMode, + Listable, + ModelReference, + Sortable, +} from "@ajs.local/data-api/beta/metadata"; +import { expect } from "chai"; +import { + getFunctionName, + getSchemaInstance, + listRequest, + request, + validateObjectList, +} from "../utils"; + +const currentTestName = path + .basename(__filename) + .replace(/\.test\.(ts|js)$/, ""); const productTableName = `products-${currentTestName}`; -const schemaName = 'default'; +const schemaName = "default"; @RegisterTable(productTableName, schemaName) class Product extends Table { @@ -36,61 +54,78 @@ class ProductModel extends BasicDataModel(Product, productTableName) {} const defaultProductDataset: Partial[] = [ { - name: 'OneSung X', - description: 'Smartphone with a 6.7-inch display based on an Tensilica Xtensa LX6 chip.', + name: "OneSung X", + description: + "Smartphone with a 6.7-inch display based on an Tensilica Xtensa LX6 chip.", price: 929.99, - reference: 'OS-X', - addedAt: new Date('2025-06-15'), - internalNotes: 'This should not be visible in lists', - metadata: 'idk what to put here', + reference: "OS-X", + addedAt: new Date("2025-06-15"), + internalNotes: "This should not be visible in lists", + metadata: "idk what to put here", }, { - name: 'Kine Earth Max', - description: "AI powered shoes, adjusting the sole's flexibility based on the ground", + name: "Kine Earth Max", + description: + "AI powered shoes, adjusting the sole's flexibility based on the ground", price: 129.99, - reference: 'KE-MAX', - addedAt: new Date('1900-01-01'), - internalNotes: 'This should not be visible in lists either', + reference: "KE-MAX", + addedAt: new Date("1900-01-01"), + internalNotes: "This should not be visible in lists either", metadata: "seriously, no idea what to put here. At least there's a string", }, { - name: 'Pocket Potato', - description: 'A potato that fits in your pocket', + name: "Pocket Potato", + description: "A potato that fits in your pocket", price: 1.99, - reference: 'PP-1', - addedAt: new Date('3000-12-31'), - internalNotes: 'Again, not visible in lists', - metadata: 'still does not matter', + reference: "PP-1", + addedAt: new Date("3000-12-31"), + internalNotes: "Again, not visible in lists", + metadata: "still does not matter", }, ]; -describe('Field Listable', () => { - it('default listing', async () => await defaultListing()); - it('list only detailed fields', async () => await listDetailedFields()); - it('list nonexistant pluck mode', async () => await listNonexistantPluckMode()); - it('list only 2 rows per page', async () => await listOnly2RowsPerPage()); - it('list from 2nd page', async () => await listFrom2ndPage()); - it('list only 2 first pages', async () => await listOnly2FirstPages()); - it('list only 2nd page', async () => await listOnly2ndPage()); - it('sorting by string (name), ascending', async () => await sortByNameAscending()); - it('sorting by string (name), descending', async () => await sortByNameDescending()); - it('sorting by number (price), ascending', async () => await sortByPriceAscending()); - it('sorting by number (price), descending', async () => await sortByPriceDescending()); - it('sorting by date (addedAt), ascending', async () => await sortByAddedAtAscending()); - it('sorting by date (addedAt), descending', async () => await sortByAddedAtDescending()); +describe("Field Listable", () => { + it("default listing", async () => await defaultListing()); + it("list only detailed fields", async () => await listDetailedFields()); + it("list nonexistant pluck mode", async () => + await listNonexistantPluckMode()); + it("list only 2 rows per page", async () => await listOnly2RowsPerPage()); + it("list from 2nd page", async () => await listFrom2ndPage()); + it("list only 2 first pages", async () => await listOnly2FirstPages()); + it("list only 2nd page", async () => await listOnly2ndPage()); + it("sorting by string (name), ascending", async () => + await sortByNameAscending()); + it("sorting by string (name), descending", async () => + await sortByNameDescending()); + it("sorting by number (price), ascending", async () => + await sortByPriceAscending()); + it("sorting by number (price), descending", async () => + await sortByPriceDescending()); + it("sorting by date (addedAt), ascending", async () => + await sortByAddedAtAscending()); + it("sorting by date (addedAt), descending", async () => + await sortByAddedAtDescending()); after(async () => {}); }); -async function _createDataController(testName: string, route: any, product: Partial[]) { +async function _createDataController( + testName: string, + route: any, + product: Partial[], +) { @RegisterDataController() - class _ListableTestAPI extends DataController(Product, route, Controller(`/${testName}`)) { + class _ListableTestAPI extends DataController( + Product, + route, + Controller(`/${testName}`), + ) { @ModelReference() @Model(ProductModel) declare productModel: ProductModel; @Listable() - @Listable(true, 'detailed') + @Listable(true, "detailed") @Access(AccessMode.ReadOnly) declare _id: string; @@ -116,18 +151,18 @@ async function _createDataController(testName: string, route: any, product: Part @Sortable({ noIndex: true }) declare addedAt: Date; - @Listable(true, 'detailed') + @Listable(true, "detailed") @Access(AccessMode.ReadOnly) declare metadata: string; - @Listable(true, 'detailed') - @Listable(true, 'nonexistent') + @Listable(true, "detailed") + @Listable(true, "nonexistent") @Access(AccessMode.ReadOnly) declare description: string; } await CreateDatabaseSchemaInstance(schemaName); await _dropProductTable(); - const productModel = new ProductModel(Schema.get(schemaName)!.instance()); + const productModel = new ProductModel(getSchemaInstance(schemaName)); const insertResults = await productModel.insert(product); return { ids: insertResults, productModel }; } @@ -140,7 +175,7 @@ async function _dropProductTable() { } async function _getDatabaseProducts(ids: string[], productModel: ProductModel) { - let database_products: Product[] = []; + const database_products: Product[] = []; for (const id of ids) { const product = await productModel.get(id); expect(product).to.not.equal(undefined); @@ -163,8 +198,16 @@ async function defaultListing() { const data = (await response.json()) as { results: Product[] }; expect(data.results).to.have.length(defaultProductDataset.length); const listed_products = data.results; - const database_products = await _getDatabaseProducts(Object.values(ids), productModel); - await validateObjectList(listed_products, database_products, ['_id', 'name', 'price', 'reference']); + const database_products = await _getDatabaseProducts( + Object.values(ids), + productModel, + ); + await validateObjectList(listed_products, database_products, [ + "_id", + "name", + "price", + "reference", + ]); for (const product of listed_products) { expect(product.internalNotes).to.equal(undefined); expect(product.description).to.equal(undefined); @@ -175,17 +218,34 @@ async function defaultListing() { async function listDetailedFields() { const { ids, productModel } = await _createDataController( getFunctionName(), - { detailed: DefaultRoutes.WithOptions(DefaultRoutes.List, { pluckMode: 'detailed' }) }, + { + detailed: DefaultRoutes.WithOptions(DefaultRoutes.List, { + pluckMode: "detailed", + }), + }, defaultProductDataset, ); - const response = await request(getFunctionName(), 'detailed', 'GET', undefined, {}); + const response = await request( + getFunctionName(), + "detailed", + "GET", + undefined, + {}, + ); expect(response.status).to.equal(200); const data = (await response.json()) as { results: Product[] }; expect(data.results).to.have.length(defaultProductDataset.length); const listed_products = data.results; - const database_products = await _getDatabaseProducts(Object.values(ids), productModel); - await validateObjectList(listed_products, database_products, ['_id', 'metadata', 'description']); + const database_products = await _getDatabaseProducts( + Object.values(ids), + productModel, + ); + await validateObjectList(listed_products, database_products, [ + "_id", + "metadata", + "description", + ]); for (const product of listed_products) { expect(product.internalNotes).to.equal(undefined); expect(product.name).to.equal(undefined); @@ -197,18 +257,32 @@ async function listDetailedFields() { async function listNonexistantPluckMode() { await _createDataController( getFunctionName(), - { detailed: DefaultRoutes.WithOptions(DefaultRoutes.List, { pluckMode: 'detailed' }) }, + { + detailed: DefaultRoutes.WithOptions(DefaultRoutes.List, { + pluckMode: "detailed", + }), + }, defaultProductDataset, ); - const response = await request(getFunctionName(), 'nonexistent', 'GET', undefined, {}); + const response = await request( + getFunctionName(), + "nonexistent", + "GET", + undefined, + {}, + ); expect(response.status).to.equal(404); } async function listOnly2RowsPerPage() { - await _createDataController(getFunctionName(), { list: DefaultRoutes.List }, defaultProductDataset); + await _createDataController( + getFunctionName(), + { list: DefaultRoutes.List }, + defaultProductDataset, + ); - const response = await listRequest(getFunctionName(), { limit: '2' }); + const response = await listRequest(getFunctionName(), { limit: "2" }); expect(response.status).to.equal(200); const data = (await response.json()) as { results: Record[]; @@ -225,9 +299,13 @@ async function listOnly2RowsPerPage() { } async function listFrom2ndPage() { - await _createDataController(getFunctionName(), { list: DefaultRoutes.List }, defaultProductDataset); + await _createDataController( + getFunctionName(), + { list: DefaultRoutes.List }, + defaultProductDataset, + ); - const response = await listRequest(getFunctionName(), { offset: '2' }); + const response = await listRequest(getFunctionName(), { offset: "2" }); expect(response.status).to.equal(200); const data = (await response.json()) as { results: Record[]; @@ -242,9 +320,13 @@ async function listFrom2ndPage() { } async function listOnly2FirstPages() { - await _createDataController(getFunctionName(), { list: DefaultRoutes.List }, defaultProductDataset); + await _createDataController( + getFunctionName(), + { list: DefaultRoutes.List }, + defaultProductDataset, + ); - const response = await listRequest(getFunctionName(), { maxPage: '2' }); + const response = await listRequest(getFunctionName(), { maxPage: "2" }); expect(response.status).to.equal(200); const data = (await response.json()) as { results: Record[]; @@ -258,9 +340,16 @@ async function listOnly2FirstPages() { } async function listOnly2ndPage() { - await _createDataController(getFunctionName(), { list: DefaultRoutes.List }, defaultProductDataset); + await _createDataController( + getFunctionName(), + { list: DefaultRoutes.List }, + defaultProductDataset, + ); - const response = await listRequest(getFunctionName(), { offset: '2', limit: '1' }); + const response = await listRequest(getFunctionName(), { + offset: "2", + limit: "1", + }); expect(response.status).to.equal(200); const data = (await response.json()) as { results: Record[]; @@ -275,20 +364,28 @@ async function listOnly2ndPage() { expect(data.results[0]._id).to.not.equal(undefined); } -function _getSortedField(dataset: Partial[], field: keyof Product, direction: 'asc' | 'desc') { +function _getSortedField( + dataset: Partial[], + field: keyof Product, + direction: "asc" | "desc", +) { const mappedField = dataset.map((product) => product[field]); - const filteredField = mappedField.filter((v): v is string | number | Date => v !== undefined && v !== null); + const filteredField = mappedField.filter( + (v): v is string | number | Date => v !== undefined && v !== null, + ); filteredField.sort((a, b) => { if (a instanceof Date && b instanceof Date) { - return direction === 'asc' ? a.getTime() - b.getTime() : b.getTime() - a.getTime(); + return direction === "asc" + ? a.getTime() - b.getTime() + : b.getTime() - a.getTime(); } - if (typeof a === 'number' && typeof b === 'number') { - return direction === 'asc' ? a - b : b - a; + if (typeof a === "number" && typeof b === "number") { + return direction === "asc" ? a - b : b - a; } - if (typeof a === 'string' && typeof b === 'string') { - return direction === 'asc' ? a.localeCompare(b) : b.localeCompare(a); + if (typeof a === "string" && typeof b === "string") { + return direction === "asc" ? a.localeCompare(b) : b.localeCompare(a); } return 0; }); @@ -297,9 +394,16 @@ function _getSortedField(dataset: Partial[], field: keyof Product, dire } async function sortByNameAscending() { - await _createDataController(getFunctionName(), { list: DefaultRoutes.List }, defaultProductDataset); + await _createDataController( + getFunctionName(), + { list: DefaultRoutes.List }, + defaultProductDataset, + ); - const response = await listRequest(getFunctionName(), { sortKey: 'name', sortDirection: 'asc' }); + const response = await listRequest(getFunctionName(), { + sortKey: "name", + sortDirection: "asc", + }); expect(response.status).to.equal(200); const data = (await response.json()) as { results: Record[]; @@ -310,14 +414,21 @@ async function sortByNameAscending() { expect(data.results).to.have.length(defaultProductDataset.length); const sortedNames = data.results.map((product: any) => product.name); - const expectedNames = _getSortedField(defaultProductDataset, 'name', 'asc'); + const expectedNames = _getSortedField(defaultProductDataset, "name", "asc"); expect(sortedNames).to.deep.equal(expectedNames); } async function sortByNameDescending() { - await _createDataController(getFunctionName(), { list: DefaultRoutes.List }, defaultProductDataset); + await _createDataController( + getFunctionName(), + { list: DefaultRoutes.List }, + defaultProductDataset, + ); - const response = await listRequest(getFunctionName(), { sortKey: 'name', sortDirection: 'desc' }); + const response = await listRequest(getFunctionName(), { + sortKey: "name", + sortDirection: "desc", + }); expect(response.status).to.equal(200); const data = (await response.json()) as { results: Record[]; @@ -328,14 +439,21 @@ async function sortByNameDescending() { expect(data.results).to.have.length(defaultProductDataset.length); const sortedNames = data.results.map((product: any) => product.name); - const expectedNames = _getSortedField(defaultProductDataset, 'name', 'desc'); + const expectedNames = _getSortedField(defaultProductDataset, "name", "desc"); expect(sortedNames).to.deep.equal(expectedNames); } async function sortByPriceAscending() { - await _createDataController(getFunctionName(), { list: DefaultRoutes.List }, defaultProductDataset); + await _createDataController( + getFunctionName(), + { list: DefaultRoutes.List }, + defaultProductDataset, + ); - const response = await listRequest(getFunctionName(), { sortKey: 'price', sortDirection: 'asc' }); + const response = await listRequest(getFunctionName(), { + sortKey: "price", + sortDirection: "asc", + }); expect(response.status).to.equal(200); const data = (await response.json()) as { results: Record[]; @@ -346,14 +464,21 @@ async function sortByPriceAscending() { expect(data.results).to.have.length(defaultProductDataset.length); const sortedPrices = data.results.map((product: any) => product.price); - const expectedPrices = _getSortedField(defaultProductDataset, 'price', 'asc'); + const expectedPrices = _getSortedField(defaultProductDataset, "price", "asc"); expect(sortedPrices).to.deep.equal(expectedPrices); } async function sortByPriceDescending() { - await _createDataController(getFunctionName(), { list: DefaultRoutes.List }, defaultProductDataset); + await _createDataController( + getFunctionName(), + { list: DefaultRoutes.List }, + defaultProductDataset, + ); - const response = await listRequest(getFunctionName(), { sortKey: 'price', sortDirection: 'desc' }); + const response = await listRequest(getFunctionName(), { + sortKey: "price", + sortDirection: "desc", + }); expect(response.status).to.equal(200); const data = (await response.json()) as { results: Record[]; @@ -364,14 +489,25 @@ async function sortByPriceDescending() { expect(data.results).to.have.length(defaultProductDataset.length); const sortedPrices = data.results.map((product: any) => product.price); - const expectedPrices = _getSortedField(defaultProductDataset, 'price', 'desc'); + const expectedPrices = _getSortedField( + defaultProductDataset, + "price", + "desc", + ); expect(sortedPrices).to.deep.equal(expectedPrices); } async function sortByAddedAtAscending() { - await _createDataController(getFunctionName(), { list: DefaultRoutes.List }, defaultProductDataset); + await _createDataController( + getFunctionName(), + { list: DefaultRoutes.List }, + defaultProductDataset, + ); - const response = await listRequest(getFunctionName(), { sortKey: 'addedAt', sortDirection: 'asc' }); + const response = await listRequest(getFunctionName(), { + sortKey: "addedAt", + sortDirection: "asc", + }); expect(response.status).to.equal(200); const data = (await response.json()) as { results: Record[]; @@ -380,17 +516,26 @@ async function sortByAddedAtAscending() { limit: number; }; expect(data.results).to.have.length(defaultProductDataset.length); - const sortedDates = data.results.map((product: any) => new Date(product.addedAt).getTime()); - const expectedDates = _getSortedField(defaultProductDataset, 'addedAt', 'asc') + const sortedDates = data.results.map((product: any) => + new Date(product.addedAt).getTime(), + ); + const expectedDates = _getSortedField(defaultProductDataset, "addedAt", "asc") .filter((value): value is Date => value instanceof Date) .map((date: Date) => date.getTime()); expect(sortedDates).to.deep.equal(expectedDates); } async function sortByAddedAtDescending() { - await _createDataController(getFunctionName(), { list: DefaultRoutes.List }, defaultProductDataset); + await _createDataController( + getFunctionName(), + { list: DefaultRoutes.List }, + defaultProductDataset, + ); - const response = await listRequest(getFunctionName(), { sortKey: 'addedAt', sortDirection: 'desc' }); + const response = await listRequest(getFunctionName(), { + sortKey: "addedAt", + sortDirection: "desc", + }); expect(response.status).to.equal(200); const data = (await response.json()) as { results: Record[]; @@ -400,8 +545,14 @@ async function sortByAddedAtDescending() { }; expect(data.results).to.have.length(defaultProductDataset.length); - const sortedDates = data.results.map((product: any) => new Date(product.addedAt).getTime()); - const expectedDates = _getSortedField(defaultProductDataset, 'addedAt', 'desc') + const sortedDates = data.results.map((product: any) => + new Date(product.addedAt).getTime(), + ); + const expectedDates = _getSortedField( + defaultProductDataset, + "addedAt", + "desc", + ) .filter((value): value is Date => value instanceof Date) .map((date: Date) => date.getTime()); expect(sortedDates).to.deep.equal(expectedDates); diff --git a/src/test/interfaces/data-api/beta/components/mandatory.test.ts b/src/test/interfaces/data-api/beta/components/mandatory.test.ts index 093e1c3..fb24f6b 100644 --- a/src/test/interfaces/data-api/beta/components/mandatory.test.ts +++ b/src/test/interfaces/data-api/beta/components/mandatory.test.ts @@ -1,22 +1,40 @@ -import { expect } from 'chai'; -import { Schema } from '@ajs/database/beta'; +import path from "node:path"; +import { Controller } from "@ajs/api/beta"; +import { Schema } from "@ajs/database/beta"; import { - Table, - Index, - RegisterTable, - CreateDatabaseSchemaInstance, BasicDataModel, + CreateDatabaseSchemaInstance, + Index, Model, -} from '@ajs/database-decorators/beta'; -import { Controller } from '@ajs/api/beta'; -import { DataController, DefaultRoutes, RegisterDataController } from '@ajs.local/data-api/beta'; -import { Access, AccessMode, Mandatory, ModelReference } from '@ajs.local/data-api/beta/metadata'; -import { editRequest, getFunctionName, newRequest, request, validateObject } from '../utils'; -import path from 'node:path'; - -const currentTestName = path.basename(__filename).replace(/\.test\.(ts|js)$/, ''); + RegisterTable, + Table, +} from "@ajs/database-decorators/beta"; +import { + DataController, + DefaultRoutes, + RegisterDataController, +} from "@ajs.local/data-api/beta"; +import { + Access, + AccessMode, + Mandatory, + ModelReference, +} from "@ajs.local/data-api/beta/metadata"; +import { expect } from "chai"; +import { + editRequest, + getFunctionName, + getSchemaInstance, + newRequest, + request, + validateObject, +} from "../utils"; + +const currentTestName = path + .basename(__filename) + .replace(/\.test\.(ts|js)$/, ""); const orderTableName = `orders-${currentTestName}`; -const schemaName = 'default'; +const schemaName = "default"; @RegisterTable(orderTableName, schemaName) class Order extends Table { @@ -35,29 +53,34 @@ class OrderModel extends BasicDataModel(Order, orderTableName) {} const validOrderDataset: Record> = { default: { - customerName: 'Bob', - customerEmail: 'bob@example.com', + customerName: "Bob", + customerEmail: "bob@example.com", totalAmount: 99.99, - status: 'pending', - notes: 'Customer requested express shipping', - internalReference: 'INT-REF-001', + status: "pending", + notes: "Customer requested express shipping", + internalReference: "INT-REF-001", }, alternative: { - customerName: 'Alice', - customerEmail: 'alice@example.com', + customerName: "Alice", + customerEmail: "alice@example.com", totalAmount: 149.99, - status: 'processing', - notes: 'Customer requested express shipping', - internalReference: 'INT-REF-002', + status: "processing", + notes: "Customer requested express shipping", + internalReference: "INT-REF-002", }, }; -describe('Field Mandatory', () => { - it('new row with all mandatory fields', async () => await newWithAllMandatoryFields()); - it('new row with missing mandatory fields', async () => await newWithMissingMandatoryFields()); - it('edit row with all mandatory fields', async () => await editWithAllMandatoryFields()); - it('edit row with missing mandatory fields', async () => await editWithMissingMandatoryFields()); - it('skip mandatory validation when noMandatory is true', async () => await skipMandatoryValidationWhenNoMandatory()); +describe("Field Mandatory", () => { + it("new row with all mandatory fields", async () => + await newWithAllMandatoryFields()); + it("new row with missing mandatory fields", async () => + await newWithMissingMandatoryFields()); + it("edit row with all mandatory fields", async () => + await editWithAllMandatoryFields()); + it("edit row with missing mandatory fields", async () => + await editWithMissingMandatoryFields()); + it("skip mandatory validation when noMandatory is true", async () => + await skipMandatoryValidationWhenNoMandatory()); after(async () => {}); }); @@ -69,9 +92,17 @@ async function _dropOrderTable() { } } -async function _createDataController(testName: string, route: any, order?: Partial) { +async function _createDataController( + testName: string, + route: any, + order?: Partial, +) { @RegisterDataController() - class _MandatoryTestAPI extends DataController(Order, route, Controller(`/${testName}`)) { + class _MandatoryTestAPI extends DataController( + Order, + route, + Controller(`/${testName}`), + ) { @ModelReference() @Model(OrderModel) declare orderModel: OrderModel; @@ -79,19 +110,19 @@ async function _createDataController(testName: string, route: any, order?: Parti declare _id: string; @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') + @Mandatory("new", "edit") declare customerName: string; @Access(AccessMode.ReadWrite) - @Mandatory('new') + @Mandatory("new") declare customerEmail: string; @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') + @Mandatory("new", "edit") declare totalAmount: number; @Access(AccessMode.ReadWrite) - @Mandatory('edit') + @Mandatory("edit") declare status: string; @Access(AccessMode.ReadWrite) @@ -102,7 +133,7 @@ async function _createDataController(testName: string, route: any, order?: Parti } await CreateDatabaseSchemaInstance(schemaName); await _dropOrderTable(); - const orderModel = new OrderModel(Schema.get(schemaName)!.instance()); + const orderModel = new OrderModel(getSchemaInstance(schemaName)); if (order) { const insertResult = await orderModel.insert(order); @@ -125,13 +156,17 @@ async function newWithAllMandatoryFields() { }); expect(response.status).to.equal(200); const result = (await response.json()) as string[]; - expect(result).to.be.an('array'); + expect(result).to.be.an("array"); expect(result).to.have.length(1); - expect(result[0]).to.be.a('string'); + expect(result[0]).to.be.a("string"); const order = await orderModel.get(result[0]); expect(order).to.not.equal(undefined); if (order) { - await validateObject(order, validOrderDataset.default, ['customerName', 'customerEmail', 'totalAmount']); + await validateObject(order, validOrderDataset.default, [ + "customerName", + "customerEmail", + "totalAmount", + ]); } } @@ -144,7 +179,7 @@ async function newWithMissingMandatoryFields() { }); expect(response.status).to.equal(400); const result = await response.text(); - expect(result).to.include('Missing mandatory fields: customerEmail'); + expect(result).to.include("Missing mandatory fields: customerEmail"); } async function editWithAllMandatoryFields() { @@ -153,6 +188,7 @@ async function editWithAllMandatoryFields() { { edit: DefaultRoutes.Edit }, validOrderDataset.default, ); + if (!id) throw new Error("Expected id from _createDataController"); const response = await editRequest( getFunctionName(), @@ -161,15 +197,24 @@ async function editWithAllMandatoryFields() { totalAmount: validOrderDataset.alternative.totalAmount, status: validOrderDataset.alternative.status, }, - { id: id! }, + { id }, ); expect(response.status).to.equal(200); - const order = await orderModel.get(id!); - expect(order).to.be.an('object'); - expect(order).to.have.property('customerName', validOrderDataset.alternative.customerName); - expect(order).to.have.property('totalAmount', validOrderDataset.alternative.totalAmount); - expect(order).to.have.property('status', validOrderDataset.alternative.status); + const order = await orderModel.get(id); + expect(order).to.be.an("object"); + expect(order).to.have.property( + "customerName", + validOrderDataset.alternative.customerName, + ); + expect(order).to.have.property( + "totalAmount", + validOrderDataset.alternative.totalAmount, + ); + expect(order).to.have.property( + "status", + validOrderDataset.alternative.status, + ); } async function editWithMissingMandatoryFields() { @@ -178,6 +223,7 @@ async function editWithMissingMandatoryFields() { { edit: DefaultRoutes.Edit }, validOrderDataset.default, ); + if (!id) throw new Error("Expected id from _createDataController"); const response = await editRequest( getFunctionName(), @@ -185,24 +231,35 @@ async function editWithMissingMandatoryFields() { customerName: validOrderDataset.alternative.customerName, totalAmount: validOrderDataset.alternative.totalAmount, }, - { id: id! }, + { id }, ); expect(response.status).to.equal(400); const text = await response.text(); - expect(text).to.include('Missing mandatory fields: status'); + expect(text).to.include("Missing mandatory fields: status"); } async function skipMandatoryValidationWhenNoMandatory() { const { id, orderModel } = await _createDataController( getFunctionName(), - { editNoMandatory: DefaultRoutes.WithOptions(DefaultRoutes.Edit, { noMandatory: 'true' }) }, + { + editNoMandatory: DefaultRoutes.WithOptions(DefaultRoutes.Edit, { + noMandatory: "true", + }), + }, validOrderDataset.default, ); + if (!id) throw new Error("Expected id from _createDataController"); - const response = await request(getFunctionName(), 'editNoMandatory', 'PUT', { notes: 'Updated notes' }, { id: id! }); + const response = await request( + getFunctionName(), + "editNoMandatory", + "PUT", + { notes: "Updated notes" }, + { id }, + ); expect(response.status).to.equal(200); - const order = await orderModel.get(id!); - expect(order).to.be.an('object'); - expect(order).to.have.property('notes', 'Updated notes'); + const order = await orderModel.get(id); + expect(order).to.be.an("object"); + expect(order).to.have.property("notes", "Updated notes"); } diff --git a/src/test/interfaces/data-api/beta/components/per_action_access.test.ts b/src/test/interfaces/data-api/beta/components/per_action_access.test.ts index feaf832..03102ff 100644 --- a/src/test/interfaces/data-api/beta/components/per_action_access.test.ts +++ b/src/test/interfaces/data-api/beta/components/per_action_access.test.ts @@ -1,22 +1,38 @@ -import { expect } from 'chai'; -import { Schema } from '@ajs/database/beta'; +import path from "node:path"; +import { Controller } from "@ajs/api/beta"; import { - Table, - Index, - RegisterTable, - CreateDatabaseSchemaInstance, BasicDataModel, + CreateDatabaseSchemaInstance, + Index, Model, -} from '@ajs/database-decorators/beta'; -import { Controller } from '@ajs/api/beta'; -import { DataController, DefaultRoutes, RegisterDataController } from '@ajs.local/data-api/beta'; -import { Access, AccessMode, Listable, ModelReference } from '@ajs.local/data-api/beta/metadata'; -import { editRequest, getFunctionName, getRequest, newRequest } from '../utils'; -import path from 'node:path'; - -const currentTestName = path.basename(__filename).replace(/\.test\.(ts|js)$/, ''); + RegisterTable, + Table, +} from "@ajs/database-decorators/beta"; +import { + DataController, + DefaultRoutes, + RegisterDataController, +} from "@ajs.local/data-api/beta"; +import { + Access, + AccessMode, + Listable, + ModelReference, +} from "@ajs.local/data-api/beta/metadata"; +import { expect } from "chai"; +import { + editRequest, + getFunctionName, + getRequest, + getSchemaInstance, + newRequest, +} from "../utils"; + +const currentTestName = path + .basename(__filename) + .replace(/\.test\.(ts|js)$/, ""); const userTableName = `users-${currentTestName}`; -const schemaName = 'default'; +const schemaName = "default"; @RegisterTable(userTableName, schemaName) class User extends Table { @@ -32,22 +48,28 @@ class User extends Table { class UserModel extends BasicDataModel(User, userTableName) {} const defaultUserDataset: Partial = { - name: 'Jean Test', - email: 'jean.test@email.com', - role: 'admin', + name: "Jean Test", + email: "jean.test@email.com", + role: "admin", age: 30, }; -describe('Per-Action Access Control', () => { - it('writable in new but read-only in edit', async () => await writableInNewReadOnlyInEdit()); - it('read-only globally but read-write in new', async () => await readOnlyGloballyReadWriteInNew()); +describe("Per-Action Access Control", () => { + it("writable in new but read-only in edit", async () => + await writableInNewReadOnlyInEdit()); + it("read-only globally but read-write in new", async () => + await readOnlyGloballyReadWriteInNew()); after(async () => {}); }); async function _createDataController(testName: string, user: Partial) { @RegisterDataController() - class _PerActionAccessAPI extends DataController(User, DefaultRoutes.All, Controller(`/${testName}`)) { + class _PerActionAccessAPI extends DataController( + User, + DefaultRoutes.All, + Controller(`/${testName}`), + ) { @ModelReference() @Model(UserModel) declare userModel: UserModel; @@ -72,27 +94,33 @@ async function _createDataController(testName: string, user: Partial) { declare age: number; } await CreateDatabaseSchemaInstance(schemaName); - const database = Schema.get(schemaName)!.instance(); + const database = getSchemaInstance(schemaName); const userModel = new UserModel(database); const insertResult = await userModel.insert(user); return { id: insertResult[0], userModel }; } async function writableInNewReadOnlyInEdit() { - const { id, userModel } = await _createDataController(getFunctionName(), defaultUserDataset); + const { id, userModel } = await _createDataController( + getFunctionName(), + defaultUserDataset, + ); const getResponse = await getRequest(getFunctionName(), { id }); expect(getResponse.status).to.equal(200); const data = (await getResponse.json()) as User; - expect(data.role).to.equal('admin'); + expect(data.role).to.equal("admin"); - await editRequest(getFunctionName(), { role: 'user' }, { id }); + await editRequest(getFunctionName(), { role: "user" }, { id }); const user = await userModel.get(id); - expect(user?.role).to.equal('admin'); + expect(user?.role).to.equal("admin"); } async function readOnlyGloballyReadWriteInNew() { - const { id, userModel } = await _createDataController(getFunctionName(), defaultUserDataset); + const { id, userModel } = await _createDataController( + getFunctionName(), + defaultUserDataset, + ); const newResponse = await newRequest(getFunctionName(), defaultUserDataset); expect(newResponse.status).to.equal(200); diff --git a/src/test/interfaces/data-api/beta/components/validator.test.ts b/src/test/interfaces/data-api/beta/components/validator.test.ts index 28eab9f..7545c5d 100644 --- a/src/test/interfaces/data-api/beta/components/validator.test.ts +++ b/src/test/interfaces/data-api/beta/components/validator.test.ts @@ -1,22 +1,37 @@ -import { expect } from 'chai'; -import { Schema } from '@ajs/database/beta'; +import path from "node:path"; +import { Controller } from "@ajs/api/beta"; import { - Table, - Index, - RegisterTable, - CreateDatabaseSchemaInstance, BasicDataModel, + CreateDatabaseSchemaInstance, + Index, Model, -} from '@ajs/database-decorators/beta'; -import { Controller } from '@ajs/api/beta'; -import { DataController, DefaultRoutes, RegisterDataController } from '@ajs.local/data-api/beta'; -import { Validator, ModelReference, AccessMode, Access } from '@ajs.local/data-api/beta/metadata'; -import { editRequest, newRequest, validateObject } from '../utils'; -import path from 'node:path'; + RegisterTable, + Table, +} from "@ajs/database-decorators/beta"; +import { + DataController, + DefaultRoutes, + RegisterDataController, +} from "@ajs.local/data-api/beta"; +import { + Access, + AccessMode, + ModelReference, + Validator, +} from "@ajs.local/data-api/beta/metadata"; +import { expect } from "chai"; +import { + editRequest, + getSchemaInstance, + newRequest, + validateObject, +} from "../utils"; -const currentTestName = path.basename(__filename).replace(/\.test\.(ts|js)$/, ''); +const currentTestName = path + .basename(__filename) + .replace(/\.test\.(ts|js)$/, ""); const productTableName = `products-${currentTestName}`; -const schemaName = 'default'; +const schemaName = "default"; @RegisterTable(productTableName, schemaName) class Product extends Table { @@ -36,41 +51,59 @@ class ProductModel extends BasicDataModel(Product, productTableName) {} const validProductData: Record> = { default: { - name: 'Valid Product', + name: "Valid Product", price: 29.99, - email: 'test@example.com', - birthDate: new Date('1990-01-01'), - status: 'active', - tags: ['electronics', 'gadgets'], + email: "test@example.com", + birthDate: new Date("1990-01-01"), + status: "active", + tags: ["electronics", "gadgets"], }, alternative: { - name: 'Valid Product', + name: "Valid Product", price: 29.99, - email: 'test@example.com', - birthDate: new Date('1990-01-01'), - status: 'active', - tags: ['electronics', 'gadgets'], + email: "test@example.com", + birthDate: new Date("1990-01-01"), + status: "active", + tags: ["electronics", "gadgets"], }, }; -describe('Field Validator', () => { - it('validate correct parameters on new', async () => await validateCorrectParametersOnNew()); - it('validate incorrect date parameter on new', async () => await validateIncorrectDateParameterOnNew()); - it('validate incorrect regex parameter on new', async () => await validateIncorrectEmailParameterOnNew()); - it('validate incorrect string parameter on new', async () => await validateIncorrectStringParameterOnNew()); - it('validate incorrect number parameter on new', async () => await validateIncorrectNumberParameterOnNew()); - it('validate correct parameters on edit', async () => await validateCorrectParametersOnEdit()); - it('validate incorrect date parameter on edit', async () => await validateIncorrectDateParameterOnEdit()); - it('validate incorrect regex parameter on edit', async () => await validateIncorrectEmailParameterOnEdit()); - it('validate incorrect string parameter on edit', async () => await validateIncorrectStringParameterOnEdit()); - it('validate incorrect number parameter on edit', async () => await validateIncorrectNumberParameterOnEdit()); +describe("Field Validator", () => { + it("validate correct parameters on new", async () => + await validateCorrectParametersOnNew()); + it("validate incorrect date parameter on new", async () => + await validateIncorrectDateParameterOnNew()); + it("validate incorrect regex parameter on new", async () => + await validateIncorrectEmailParameterOnNew()); + it("validate incorrect string parameter on new", async () => + await validateIncorrectStringParameterOnNew()); + it("validate incorrect number parameter on new", async () => + await validateIncorrectNumberParameterOnNew()); + it("validate correct parameters on edit", async () => + await validateCorrectParametersOnEdit()); + it("validate incorrect date parameter on edit", async () => + await validateIncorrectDateParameterOnEdit()); + it("validate incorrect regex parameter on edit", async () => + await validateIncorrectEmailParameterOnEdit()); + it("validate incorrect string parameter on edit", async () => + await validateIncorrectStringParameterOnEdit()); + it("validate incorrect number parameter on edit", async () => + await validateIncorrectNumberParameterOnEdit()); after(async () => {}); }); -async function _createDataController(testName: string, route: any, product?: Partial) { +async function _createDataController( + testName: string, + route: any, + product?: Partial, +) { @RegisterDataController() - class _ValidatorTestAPI extends DataController(Product, route, Controller(`/${testName}`)) { + class _ValidatorTestAPI extends DataController( + Product, + route, + Controller(`/${testName}`), + ) { @ModelReference() @Model(ProductModel) declare productModel: ProductModel; @@ -78,35 +111,43 @@ async function _createDataController(testName: string, route: any, product?: Par declare _id: string; @Access(AccessMode.ReadWrite) - @Validator((value) => typeof value === 'string' && value.length >= 3) + @Validator((value) => typeof value === "string" && value.length >= 3) declare name: string; @Access(AccessMode.ReadWrite) - @Validator((value) => typeof value === 'number' && value >= 0) + @Validator((value) => typeof value === "number" && value >= 0) declare price: number; @Access(AccessMode.ReadWrite) @Validator((value) => { - if (typeof value !== 'string') return false; + if (typeof value !== "string") return false; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(value); }) declare email: string; @Access(AccessMode.ReadWrite) - @Validator((value) => typeof value === 'string' && !isNaN(Date.parse(value))) + @Validator( + (value) => typeof value === "string" && !Number.isNaN(Date.parse(value)), + ) declare birthDate: Date; @Access(AccessMode.ReadWrite) - @Validator((value) => ['active', 'inactive', 'pending'].includes(value as string)) + @Validator((value) => + ["active", "inactive", "pending"].includes(value as string), + ) declare status: string; @Access(AccessMode.ReadWrite) - @Validator((value) => Array.isArray(value) && value.every((tag) => typeof tag === 'string' && tag.length > 0)) + @Validator( + (value) => + Array.isArray(value) && + value.every((tag) => typeof tag === "string" && tag.length > 0), + ) declare tags: string[]; } await CreateDatabaseSchemaInstance(schemaName); - const productModel = new ProductModel(Schema.get(schemaName)!.instance()); + const productModel = new ProductModel(getSchemaInstance(schemaName)); if (product) { const insertResult = await productModel.insert(product); @@ -119,7 +160,11 @@ function createIncorrectValidator( testName: string, fieldName: string, invalidValue: any, - requestFunction: (testName: string, data: any, id?: Record) => Promise, + requestFunction: ( + testName: string, + data: any, + id?: Record, + ) => Promise, route: any, testDataset: Partial, createDataset?: Partial, @@ -128,7 +173,11 @@ function createIncorrectValidator( const { id } = await _createDataController(testName, route, createDataset); const invalidData = { ...testDataset, [fieldName]: invalidValue }; - const response = await requestFunction(testName, invalidData, id ? { id } : {}); + const response = await requestFunction( + testName, + invalidData, + id ? { id } : {}, + ); expect(response.status).to.equal(400); const error = await response.text(); @@ -138,15 +187,27 @@ function createIncorrectValidator( function createCorrectValidator( testName: string, - requestFunction: (testName: string, data: any, id?: Record) => Promise, + requestFunction: ( + testName: string, + data: any, + id?: Record, + ) => Promise, route: any, testDataset: Partial, createDataset?: Partial, ) { return async () => { - const { id, productModel } = await _createDataController(testName, route, createDataset); + const { id, productModel } = await _createDataController( + testName, + route, + createDataset, + ); - const response = await requestFunction(testName, testDataset, id ? { id } : {}); + const response = await requestFunction( + testName, + testDataset, + id ? { id } : {}, + ); expect(response.status).to.equal(200); let product_fetched: Product | undefined; @@ -154,14 +215,20 @@ function createCorrectValidator( product_fetched = await productModel.get(id); } else { const result = (await response.json()) as string[]; - expect(result).to.be.an('array'); + expect(result).to.be.an("array"); expect(result).to.have.length(1); - expect(result[0]).to.be.an('string'); + expect(result[0]).to.be.an("string"); product_fetched = await productModel.get(result[0]); } expect(product_fetched).to.not.equal(undefined); if (product_fetched) { - await validateObject(product_fetched, testDataset, ['email', 'name', 'price', 'status', 'tags']); + await validateObject(product_fetched, testDataset, [ + "email", + "name", + "price", + "status", + "tags", + ]); } }; } @@ -200,64 +267,64 @@ function createIncorrectValidatorNew( } const validateCorrectParametersOnNew = createCorrectValidator( - 'CorrectParametersOnNew', + "CorrectParametersOnNew", newRequest, { new: DefaultRoutes.New }, validProductData.default, ); const validateIncorrectDateParameterOnNew = createIncorrectValidatorNew( - 'DateParameterOnNew', - 'birthDate', - 'invalid', + "DateParameterOnNew", + "birthDate", + "invalid", newRequest, ); const validateIncorrectEmailParameterOnNew = createIncorrectValidatorNew( - 'EmailParameterOnNew', - 'email', - 'invalid', + "EmailParameterOnNew", + "email", + "invalid", newRequest, ); const validateIncorrectStringParameterOnNew = createIncorrectValidatorNew( - 'StringParameterOnNew', - 'name', - 'ab', + "StringParameterOnNew", + "name", + "ab", newRequest, ); const validateIncorrectNumberParameterOnNew = createIncorrectValidatorNew( - 'NumberParameterOnNew', - 'price', + "NumberParameterOnNew", + "price", -10, newRequest, ); const validateCorrectParametersOnEdit = createCorrectValidator( - 'CorrectParametersOnEdit', + "CorrectParametersOnEdit", editRequest, { edit: DefaultRoutes.Edit }, validProductData.alternative, validProductData.default, ); const validateIncorrectDateParameterOnEdit = createIncorrectValidatorEdit( - 'DateParameterOnEdit', - 'birthDate', - 'invalid-date', + "DateParameterOnEdit", + "birthDate", + "invalid-date", editRequest, ); const validateIncorrectEmailParameterOnEdit = createIncorrectValidatorEdit( - 'EmailParameterOnEdit', - 'email', - 'invalid-email@a', + "EmailParameterOnEdit", + "email", + "invalid-email@a", editRequest, ); const validateIncorrectStringParameterOnEdit = createIncorrectValidatorEdit( - 'StringParameterOnEdit', - 'name', - 'ab', + "StringParameterOnEdit", + "name", + "ab", editRequest, ); const validateIncorrectNumberParameterOnEdit = createIncorrectValidatorEdit( - 'NumberParameterOnEdit', - 'price', + "NumberParameterOnEdit", + "price", -10, editRequest, ); diff --git a/src/test/interfaces/data-api/beta/index/routes.test.ts b/src/test/interfaces/data-api/beta/index/routes.test.ts index 1165678..9fd549b 100644 --- a/src/test/interfaces/data-api/beta/index/routes.test.ts +++ b/src/test/interfaces/data-api/beta/index/routes.test.ts @@ -1,22 +1,41 @@ -import { expect } from 'chai'; -import { Schema } from '@ajs/database/beta'; +import path from "node:path"; +import { Controller } from "@ajs/api/beta"; +import { Schema } from "@ajs/database/beta"; import { - Table, - Index, - RegisterTable, - CreateDatabaseSchemaInstance, BasicDataModel, + CreateDatabaseSchemaInstance, + Index, Model, -} from '@ajs/database-decorators/beta'; -import { Controller } from '@ajs/api/beta'; -import { DataController, DefaultRoutes, RegisterDataController } from '@ajs.local/data-api/beta'; -import { Access, AccessMode, Listable, ModelReference } from '@ajs.local/data-api/beta/metadata'; -import { deleteRequest, editRequest, getFunctionName, getRequest, listRequest, newRequest } from '../utils'; -import path from 'node:path'; - -const currentTestName = path.basename(__filename).replace(/\.test\.(ts|js)$/, ''); + RegisterTable, + Table, +} from "@ajs/database-decorators/beta"; +import { + DataController, + DefaultRoutes, + RegisterDataController, +} from "@ajs.local/data-api/beta"; +import { + Access, + AccessMode, + Listable, + ModelReference, +} from "@ajs.local/data-api/beta/metadata"; +import { expect } from "chai"; +import { + deleteRequest, + editRequest, + getFunctionName, + getRequest, + getSchemaInstance, + listRequest, + newRequest, +} from "../utils"; + +const currentTestName = path + .basename(__filename) + .replace(/\.test\.(ts|js)$/, ""); const userTableName = `users-${currentTestName}`; -const schemaName = 'default'; +const schemaName = "default"; @RegisterTable(userTableName, schemaName) class User extends Table { @@ -33,43 +52,50 @@ class UserModel extends BasicDataModel(User, userTableName) {} const validUserDataset: Record> = { default: { - name: 'Bob', - email: 'bob@email.com', + name: "Bob", + email: "bob@email.com", age: 30, - password: 'very-secure-qwerty123', + password: "very-secure-qwerty123", }, alternative: { - name: 'Alice', - email: 'alice@email.com', + name: "Alice", + email: "alice@email.com", age: 25, - password: 'very-secure-qwerty123', + password: "very-secure-qwerty123", }, }; -describe('Routes', () => { +describe("Routes", () => { beforeEach(async () => { try { - const db = Schema.get(schemaName)!.instance(); - await db.table(userTableName).delete(); + await Schema.get(schemaName)?.instance().table(userTableName).delete(); } catch { // Instance may not exist yet on first run } }); - it('accessing default routes', async () => requestingDefaultRoutes()); - it('accessing undefined route', async () => await requestingUndefinedRoute()); - it('using route get', async () => await usingRouteGet()); - it('using route list', async () => await usingRouteList()); - it('using route new', async () => await usingRouteNew()); - it('using route edit', async () => await usingRouteEdit()); - it('using route delete', async () => await usingRouteDelete()); + it("accessing default routes", async () => requestingDefaultRoutes()); + it("accessing undefined route", async () => await requestingUndefinedRoute()); + it("using route get", async () => await usingRouteGet()); + it("using route list", async () => await usingRouteList()); + it("using route new", async () => await usingRouteNew()); + it("using route edit", async () => await usingRouteEdit()); + it("using route delete", async () => await usingRouteDelete()); after(async () => {}); }); -async function _createDataController(testName: string, user: Partial, routes?: any) { +async function _createDataController( + testName: string, + user: Partial, + routes?: any, +) { @RegisterDataController() - class _AccessTestAPI extends DataController(User, routes ?? DefaultRoutes.All, Controller(`/${testName}`)) { + class _AccessTestAPI extends DataController( + User, + routes ?? DefaultRoutes.All, + Controller(`/${testName}`), + ) { @ModelReference() @Model(UserModel) declare userModel: UserModel; @@ -95,7 +121,7 @@ async function _createDataController(testName: string, user: Partial, rout declare email: string; } await CreateDatabaseSchemaInstance(schemaName); - const userModel = new UserModel(Schema.get(schemaName)!.instance()); + const userModel = new UserModel(getSchemaInstance(schemaName)); const insertResult = await userModel.insert(user); return { id: insertResult[0], userModel }; } @@ -103,7 +129,13 @@ async function _createDataController(testName: string, user: Partial, rout async function requestingDefaultRoutes() { await _createDataController(getFunctionName(), validUserDataset.default); - const [get_response, list_response, new_response, edit_response, delete_response] = await Promise.all([ + const [ + get_response, + list_response, + new_response, + edit_response, + delete_response, + ] = await Promise.all([ getRequest(getFunctionName(), {}), listRequest(getFunctionName(), {}), newRequest(getFunctionName(), {}), @@ -125,7 +157,13 @@ async function requestingUndefinedRoute() { delete: DefaultRoutes.Delete, }); - const [get_response, list_response, new_response, edit_response, delete_response] = await Promise.all([ + const [ + get_response, + list_response, + new_response, + edit_response, + delete_response, + ] = await Promise.all([ getRequest(getFunctionName(), {}), listRequest(getFunctionName(), {}), newRequest(getFunctionName(), {}), @@ -141,14 +179,18 @@ async function requestingUndefinedRoute() { expect(delete_response.status).to.not.equal(404); } -async function validateUserUsingResponse(response: Response, id: string, validDataset: Partial) { +async function validateUserUsingResponse( + response: Response, + id: string, + validDataset: Partial, +) { const data = (await response.json()) as { results: User[] }; expect(response.status).to.equal(200); let found: User | undefined; if (data.results && Array.isArray(data.results)) { for (const user of data.results) { - if (user._id == id) { + if (user._id === id) { found = user; break; } @@ -157,7 +199,11 @@ async function validateUserUsingResponse(response: Response, id: string, validDa await validateUser(found, validDataset); } -async function validateUserUsingModel(userModel: UserModel, id: string, validDataset: Partial) { +async function validateUserUsingModel( + userModel: UserModel, + id: string, + validDataset: Partial, +) { const user = await userModel.get(id); await validateUser(user, validDataset, id); } @@ -167,18 +213,23 @@ async function validateUser( validDataset: Partial, id?: string, ) { - const foundDataset = user instanceof UserModel ? await user.get(id!) : user; - expect(foundDataset).to.be.an('object'); - expect(foundDataset).to.have.property('name', validDataset.name); - expect(foundDataset).to.have.property('email', validDataset.email); - expect(foundDataset).to.have.property('age', validDataset.age); - expect(foundDataset).to.have.property('password', validDataset.password); + const foundDataset = + user instanceof UserModel ? await user.get(id ?? "") : user; + expect(foundDataset).to.be.an("object"); + expect(foundDataset).to.have.property("name", validDataset.name); + expect(foundDataset).to.have.property("email", validDataset.email); + expect(foundDataset).to.have.property("age", validDataset.age); + expect(foundDataset).to.have.property("password", validDataset.password); } async function usingRouteGet() { - const { id, userModel } = await _createDataController(getFunctionName(), validUserDataset.default, { - get: DefaultRoutes.Get, - }); + const { id, userModel } = await _createDataController( + getFunctionName(), + validUserDataset.default, + { + get: DefaultRoutes.Get, + }, + ); const response = await getRequest(getFunctionName(), { id }); expect(response.status).to.equal(200); @@ -186,9 +237,13 @@ async function usingRouteGet() { } async function usingRouteList() { - const { id } = await _createDataController(getFunctionName(), validUserDataset.default, { - list: DefaultRoutes.List, - }); + const { id } = await _createDataController( + getFunctionName(), + validUserDataset.default, + { + list: DefaultRoutes.List, + }, + ); const response = await listRequest(getFunctionName(), {}); expect(response.status).to.equal(200); @@ -196,28 +251,43 @@ async function usingRouteList() { } async function usingRouteNew() { - const { userModel } = await _createDataController(getFunctionName(), validUserDataset.default, { - new: DefaultRoutes.New, - }); - - const response = await newRequest(getFunctionName(), validUserDataset.default); + const { userModel } = await _createDataController( + getFunctionName(), + validUserDataset.default, + { + new: DefaultRoutes.New, + }, + ); + + const response = await newRequest( + getFunctionName(), + validUserDataset.default, + ); const result = (await response.json()) as string[]; await validateUserUsingModel(userModel, result[0], validUserDataset.default); } async function usingRouteEdit() { - const { id, userModel } = await _createDataController(getFunctionName(), validUserDataset.default, { - edit: DefaultRoutes.Edit, - }); + const { id, userModel } = await _createDataController( + getFunctionName(), + validUserDataset.default, + { + edit: DefaultRoutes.Edit, + }, + ); await editRequest(getFunctionName(), validUserDataset.alternative, { id }); await validateUserUsingModel(userModel, id, validUserDataset.alternative); } async function usingRouteDelete() { - const { id, userModel } = await _createDataController(getFunctionName(), validUserDataset.default, { - delete: DefaultRoutes.Delete, - }); + const { id, userModel } = await _createDataController( + getFunctionName(), + validUserDataset.default, + { + delete: DefaultRoutes.Delete, + }, + ); await deleteRequest(getFunctionName(), { id }); expect(await userModel.get(id)).to.equal(undefined); diff --git a/src/test/interfaces/data-api/beta/integration.test.ts b/src/test/interfaces/data-api/beta/integration.test.ts index 12503de..c252220 100644 --- a/src/test/interfaces/data-api/beta/integration.test.ts +++ b/src/test/interfaces/data-api/beta/integration.test.ts @@ -1,33 +1,46 @@ -import { expect } from 'chai'; -import { Schema } from '@ajs/database/beta'; +import path from "node:path"; +import { Controller } from "@ajs/api/beta"; import { - Table, - Index, - RegisterTable, - CreateDatabaseSchemaInstance, BasicDataModel, + CreateDatabaseSchemaInstance, + Index, Model, -} from '@ajs/database-decorators/beta'; -import { Controller } from '@ajs/api/beta'; -import { DataController, DefaultRoutes, RegisterDataController } from '@ajs.local/data-api/beta'; + RegisterTable, + Table, +} from "@ajs/database-decorators/beta"; +import { + DataController, + DefaultRoutes, + RegisterDataController, +} from "@ajs.local/data-api/beta"; import { Access, AccessMode, Listable, + Mandatory, ModelReference, Sortable, - Mandatory, Validator, -} from '@ajs.local/data-api/beta/metadata'; -import { editRequest, getRequest, listRequest, newRequest, request, validateObject } from './utils'; -import path from 'node:path'; - -const currentTestName = path.basename(__filename).replace(/\.test\.(ts|js)$/, ''); +} from "@ajs.local/data-api/beta/metadata"; +import { expect } from "chai"; +import { + editRequest, + getRequest, + getSchemaInstance, + listRequest, + newRequest, + request, + validateObject, +} from "./utils"; + +const currentTestName = path + .basename(__filename) + .replace(/\.test\.(ts|js)$/, ""); const customerTableName = `customers-${currentTestName}`; const productTableName = `products-${currentTestName}`; const orderTableName = `orders-${currentTestName}`; const orderItemTableName = `order_items-${currentTestName}`; -const schemaName = 'default'; +const schemaName = "default"; @RegisterTable(customerTableName, schemaName) class Customer extends Table { @@ -118,113 +131,113 @@ class OrderItemModel extends BasicDataModel(OrderItem, orderItemTableName) {} const testCustomers: Partial[] = [ { - firstName: 'Bob', - lastName: 'Bobberson', - email: 'bob.bobberson@email.com', - phone: '+1234567890', - address: '123 Poutine Square', - city: 'Quebec City', - postalCode: 'G1K 1K1', - country: 'Canada', - registrationDate: new Date('2023-01-15'), + firstName: "Bob", + lastName: "Bobberson", + email: "bob.bobberson@email.com", + phone: "+1234567890", + address: "123 Poutine Square", + city: "Quebec City", + postalCode: "G1K 1K1", + country: "Canada", + registrationDate: new Date("2023-01-15"), isActive: true, loyaltyPoints: 150, - preferences: ['electronics', 'books'], + preferences: ["electronics", "books"], }, { - firstName: 'Alice', - lastName: 'Alison', - email: 'alice.alison@email.com', - phone: '+33123456789', - address: '456 Baguette Street', - city: 'Lyon', - postalCode: '69001', - country: 'France', - registrationDate: new Date('2023-03-20'), + firstName: "Alice", + lastName: "Alison", + email: "alice.alison@email.com", + phone: "+33123456789", + address: "456 Baguette Street", + city: "Lyon", + postalCode: "69001", + country: "France", + registrationDate: new Date("2023-03-20"), isActive: true, loyaltyPoints: 75, - preferences: ['fashion', 'home'], + preferences: ["fashion", "home"], }, { - firstName: 'John', - lastName: 'Doe', - email: 'john.doe@email.com', - phone: '+32123456789', - address: '789 Fries Avenue', - city: 'Brussels', - postalCode: '1000', - country: 'Belgium', - registrationDate: new Date('2023-06-10'), + firstName: "John", + lastName: "Doe", + email: "john.doe@email.com", + phone: "+32123456789", + address: "789 Fries Avenue", + city: "Brussels", + postalCode: "1000", + country: "Belgium", + registrationDate: new Date("2023-06-10"), isActive: false, loyaltyPoints: 0, - preferences: ['sports'], + preferences: ["sports"], }, ]; const testProducts: Partial[] = [ { - sku: 'LAPTOP-001', - name: 'Gaming Laptop', - description: 'High performance gaming laptop', + sku: "LAPTOP-001", + name: "Gaming Laptop", + description: "High performance gaming laptop", price: 1299.99, costPrice: 899.99, stockQuantity: 15, - category: 'electronics', - brand: 'TechCorp', - tags: ['gaming', 'laptop', 'high-performance'], - images: ['laptop1.jpg', 'laptop2.jpg'], + category: "electronics", + brand: "TechCorp", + tags: ["gaming", "laptop", "high-performance"], + images: ["laptop1.jpg", "laptop2.jpg"], isActive: true, - createdAt: new Date('2023-01-01'), - updatedAt: new Date('2023-12-01'), + createdAt: new Date("2023-01-01"), + updatedAt: new Date("2023-12-01"), }, { - sku: 'PHONE-002', - name: 'Premium Smartphone', - description: 'Professional camera smartphone', + sku: "PHONE-002", + name: "Premium Smartphone", + description: "Professional camera smartphone", price: 899.99, costPrice: 599.99, stockQuantity: 25, - category: 'electronics', - brand: 'MobileTech', - tags: ['smartphone', 'camera', 'premium'], - images: ['phone1.jpg', 'phone2.jpg'], + category: "electronics", + brand: "MobileTech", + tags: ["smartphone", "camera", "premium"], + images: ["phone1.jpg", "phone2.jpg"], isActive: true, - createdAt: new Date('2023-02-15'), - updatedAt: new Date('2023-11-15'), + createdAt: new Date("2023-02-15"), + updatedAt: new Date("2023-11-15"), }, { - sku: 'BOOK-003', - name: 'The Soldering Bible', - description: 'Complete guide to soldering components', + sku: "BOOK-003", + name: "The Soldering Bible", + description: "Complete guide to soldering components", price: 49.99, costPrice: 25.99, stockQuantity: 50, - category: 'books', - brand: 'TechBooks', - tags: ['soldering', 'electronics', 'education'], - images: ['book1.jpg'], + category: "books", + brand: "TechBooks", + tags: ["soldering", "electronics", "education"], + images: ["book1.jpg"], isActive: true, - createdAt: new Date('2023-03-01'), - updatedAt: new Date('2023-10-01'), + createdAt: new Date("2023-03-01"), + updatedAt: new Date("2023-10-01"), }, { - sku: 'SHIRT-004', - name: 'Organic Cotton T-shirt', - description: 'Comfortable organic cotton t-shirt', + sku: "SHIRT-004", + name: "Organic Cotton T-shirt", + description: "Comfortable organic cotton t-shirt", price: 29.99, costPrice: 15.99, stockQuantity: 100, - category: 'fashion', - brand: 'EcoFashion', - tags: ['cotton', 'organic', 'comfortable'], - images: ['shirt1.jpg', 'shirt2.jpg'], + category: "fashion", + brand: "EcoFashion", + tags: ["cotton", "organic", "comfortable"], + images: ["shirt1.jpg", "shirt2.jpg"], isActive: true, - createdAt: new Date('2023-04-10'), - updatedAt: new Date('2023-09-10'), + createdAt: new Date("2023-04-10"), + updatedAt: new Date("2023-09-10"), }, ]; -describe('Integration tests', () => { +describe("Integration tests", () => { before(async () => { await initializeDatabase(); }); @@ -233,19 +246,30 @@ describe('Integration tests', () => { await cleanTables(); }); - it('workflow complete of customer management', async () => await workflowCustomerManagement()); - it('workflow complete of product management', async () => await workflowProductManagement()); - it('workflow complete of order management', async () => await workflowOrderManagement()); - it('stock management and automatic update', async () => await stockManagementAndAutomaticUpdate()); - it('advanced search and filtering', async () => await advancedSearchAndFiltering()); - it('client preferences management and recommendations', async () => await clientPreferencesManagement()); - it('error management and complex validation', async () => await errorManagementAndComplexValidation()); + it("workflow complete of customer management", async () => + await workflowCustomerManagement()); + it("workflow complete of product management", async () => + await workflowProductManagement()); + it("workflow complete of order management", async () => + await workflowOrderManagement()); + it("stock management and automatic update", async () => + await stockManagementAndAutomaticUpdate()); + it("advanced search and filtering", async () => + await advancedSearchAndFiltering()); + it("client preferences management and recommendations", async () => + await clientPreferencesManagement()); + it("error management and complex validation", async () => + await errorManagementAndComplexValidation()); after(async () => {}); }); @RegisterDataController() -class _CustomerAPI extends DataController(Customer, DefaultRoutes.All, Controller('/customers')) { +class _CustomerAPI extends DataController( + Customer, + DefaultRoutes.All, + Controller("/customers"), +) { @ModelReference() @Model(CustomerModel) declare customerModel: CustomerModel; @@ -256,21 +280,21 @@ class _CustomerAPI extends DataController(Customer, DefaultRoutes.All, Controlle @Listable() @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') - @Validator((value) => typeof value === 'string' && value.length >= 2) + @Mandatory("new", "edit") + @Validator((value) => typeof value === "string" && value.length >= 2) declare firstName: string; @Listable() @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') - @Validator((value) => typeof value === 'string' && value.length >= 2) + @Mandatory("new", "edit") + @Validator((value) => typeof value === "string" && value.length >= 2) declare lastName: string; @Listable() @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') + @Mandatory("new", "edit") @Validator((value) => { - if (typeof value !== 'string') return false; + if (typeof value !== "string") return false; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(value); }) @@ -278,7 +302,7 @@ class _CustomerAPI extends DataController(Customer, DefaultRoutes.All, Controlle @Listable() @Access(AccessMode.ReadWrite) - @Validator((value) => typeof value === 'string' && value.length >= 10) + @Validator((value) => typeof value === "string" && value.length >= 10) declare phone: string; @Listable() @@ -317,7 +341,11 @@ class _CustomerAPI extends DataController(Customer, DefaultRoutes.All, Controlle } @RegisterDataController() -class _ProductAPI extends DataController(Product, DefaultRoutes.All, Controller('/products')) { +class _ProductAPI extends DataController( + Product, + DefaultRoutes.All, + Controller("/products"), +) { @ModelReference() @Model(ProductModel) declare productModel: ProductModel; @@ -328,14 +356,14 @@ class _ProductAPI extends DataController(Product, DefaultRoutes.All, Controller( @Listable() @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') - @Validator((value) => typeof value === 'string' && value.length >= 3) + @Mandatory("new", "edit") + @Validator((value) => typeof value === "string" && value.length >= 3) declare sku: string; @Listable() @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') - @Validator((value) => typeof value === 'string' && value.length >= 3) + @Mandatory("new", "edit") + @Validator((value) => typeof value === "string" && value.length >= 3) declare name: string; @Listable() @@ -344,26 +372,26 @@ class _ProductAPI extends DataController(Product, DefaultRoutes.All, Controller( @Listable() @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') - @Validator((value) => typeof value === 'number' && value >= 0) + @Mandatory("new", "edit") + @Validator((value) => typeof value === "number" && value >= 0) @Sortable({ noIndex: true }) declare price: number; @Access(AccessMode.WriteOnly) - @Validator((value) => typeof value === 'number' && value >= 0) + @Validator((value) => typeof value === "number" && value >= 0) @Sortable({ noIndex: true }) declare costPrice: number; @Listable() @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') - @Validator((value) => typeof value === 'number' && value >= 0) + @Mandatory("new", "edit") + @Validator((value) => typeof value === "number" && value >= 0) @Sortable({ noIndex: true }) declare stockQuantity: number; @Listable() @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') + @Mandatory("new", "edit") declare category: string; @Listable() @@ -394,7 +422,11 @@ class _ProductAPI extends DataController(Product, DefaultRoutes.All, Controller( } @RegisterDataController() -class _OrderAPI extends DataController(Order, DefaultRoutes.All, Controller('/orders')) { +class _OrderAPI extends DataController( + Order, + DefaultRoutes.All, + Controller("/orders"), +) { @ModelReference() @Model(OrderModel) declare orderModel: OrderModel; @@ -405,18 +437,22 @@ class _OrderAPI extends DataController(Order, DefaultRoutes.All, Controller('/or @Listable() @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') + @Mandatory("new", "edit") declare orderNumber: string; @Listable() @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') + @Mandatory("new", "edit") declare customerId: string; @Listable() @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') - @Validator((value) => ['pending', 'processing', 'shipped', 'delivered', 'cancelled'].includes(value as string)) + @Mandatory("new", "edit") + @Validator((value) => + ["pending", "processing", "shipped", "delivered", "cancelled"].includes( + value as string, + ), + ) declare status: string; @Listable() @@ -462,7 +498,11 @@ class _OrderAPI extends DataController(Order, DefaultRoutes.All, Controller('/or } @RegisterDataController() -class _OrderItemAPI extends DataController(OrderItem, DefaultRoutes.All, Controller('/order-items')) { +class _OrderItemAPI extends DataController( + OrderItem, + DefaultRoutes.All, + Controller("/order-items"), +) { @ModelReference() @Model(OrderItemModel) declare orderItemModel: OrderItemModel; @@ -473,18 +513,18 @@ class _OrderItemAPI extends DataController(OrderItem, DefaultRoutes.All, Control @Listable() @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') + @Mandatory("new", "edit") declare orderId: string; @Listable() @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') + @Mandatory("new", "edit") declare productId: string; @Listable() @Access(AccessMode.ReadWrite) - @Mandatory('new', 'edit') - @Validator((value) => typeof value === 'number' && value > 0) + @Mandatory("new", "edit") + @Validator((value) => typeof value === "number" && value > 0) declare quantity: number; @Listable() @@ -500,7 +540,7 @@ async function initializeDatabase() { } async function cleanTables() { - const db = Schema.get(schemaName)!.instance(); + const db = getSchemaInstance(schemaName); await Promise.all([ db.table(customerTableName).delete(), db.table(productTableName).delete(), @@ -510,7 +550,7 @@ async function cleanTables() { } async function createTestData() { - const db = Schema.get(schemaName)!.instance(); + const db = getSchemaInstance(schemaName); const customerModel = new CustomerModel(db); const productModel = new ProductModel(db); const orderModel = new OrderModel(db); @@ -530,46 +570,53 @@ async function workflowCustomerManagement() { const { customerModel } = await createTestData(); const newCustomer = { - firstName: 'Sophie', - lastName: 'Marrone', - email: 'sophie.marrone@email.com', - phone: '+393334445556', - address: 'Via Pasta 10', - city: 'Naples', - postalCode: '80100', - country: 'Italy', + firstName: "Sophie", + lastName: "Marrone", + email: "sophie.marrone@email.com", + phone: "+393334445556", + address: "Via Pasta 10", + city: "Naples", + postalCode: "80100", + country: "Italy", isActive: true, loyaltyPoints: 0, - preferences: ['art', 'music'], + preferences: ["art", "music"], }; - const createResponse = await newRequest('customers', newCustomer); + const createResponse = await newRequest("customers", newCustomer); expect(createResponse.status).to.equal(200); const createdIds = (await createResponse.json()) as string[]; expect(createdIds).to.have.length(1); - expect(createdIds[0]).to.be.a('string'); + expect(createdIds[0]).to.be.a("string"); - const getResponse = await getRequest('customers', { id: createdIds[0] }); + const getResponse = await getRequest("customers", { id: createdIds[0] }); expect(getResponse.status).to.equal(200); const customer = (await getResponse.json()) as Customer; - await validateObject(customer, newCustomer, ['firstName', 'lastName', 'email', 'city']); + await validateObject(customer, newCustomer, [ + "firstName", + "lastName", + "email", + "city", + ]); const updateData = { ...newCustomer, loyaltyPoints: 50, - preferences: ['art', 'music', 'travel'], + preferences: ["art", "music", "travel"], }; - const updateResponse = await editRequest('customers', updateData, { id: createdIds[0] }); + const updateResponse = await editRequest("customers", updateData, { + id: createdIds[0], + }); expect(updateResponse.status).to.equal(200); const updatedCustomer = await customerModel.get(createdIds[0]); expect(updatedCustomer?.loyaltyPoints).to.equal(50); - expect(updatedCustomer?.preferences).to.include('travel'); + expect(updatedCustomer?.preferences).to.include("travel"); - const listResponse = await listRequest('customers', { - sortKey: 'registrationDate', - sortDirection: 'desc', - limit: '2', + const listResponse = await listRequest("customers", { + sortKey: "registrationDate", + sortDirection: "desc", + limit: "2", }); expect(listResponse.status).to.equal(200); const listData = (await listResponse.json()) as { results: Customer[] }; @@ -580,43 +627,49 @@ async function workflowProductManagement() { const { productModel } = await createTestData(); const newProduct = { - sku: 'HEADPHONES-005', - name: 'Casque audio sans fil', - description: 'Casque audio haute qualité avec réduction de bruit', + sku: "HEADPHONES-005", + name: "Casque audio sans fil", + description: "Casque audio haute qualité avec réduction de bruit", price: 199.99, costPrice: 120.0, stockQuantity: 30, - category: 'electronics', - brand: 'AudioTech', - tags: ['wireless', 'noise-cancelling', 'premium'], - images: ['headphones1.jpg', 'headphones2.jpg'], + category: "electronics", + brand: "AudioTech", + tags: ["wireless", "noise-cancelling", "premium"], + images: ["headphones1.jpg", "headphones2.jpg"], isActive: true, }; - const createResponse = await newRequest('products', newProduct); + const createResponse = await newRequest("products", newProduct); expect(createResponse.status).to.equal(200); const createdIds = (await createResponse.json()) as string[]; expect(createdIds).to.have.length(1); const stockUpdate = { ...newProduct, stockQuantity: 25 }; - const updateResponse = await editRequest('products', stockUpdate, { id: createdIds[0] }); + const updateResponse = await editRequest("products", stockUpdate, { + id: createdIds[0], + }); expect(updateResponse.status).to.equal(200); const updatedProduct = await productModel.get(createdIds[0]); expect(updatedProduct?.stockQuantity).to.equal(25); - const listResponse = await listRequest('products', { - sortKey: 'price', - sortDirection: 'asc', - limit: '3', + const listResponse = await listRequest("products", { + sortKey: "price", + sortDirection: "asc", + limit: "3", }); expect(listResponse.status).to.equal(200); const listData = (await listResponse.json()) as { results: Product[] }; expect(listData.results).to.have.length(3); - const categoryResponse = await listRequest('products', { category: 'electronics' }); + const categoryResponse = await listRequest("products", { + category: "electronics", + }); expect(categoryResponse.status).to.equal(200); - const categoryData = (await categoryResponse.json()) as { results: Product[] }; + const categoryData = (await categoryResponse.json()) as { + results: Product[]; + }; expect(categoryData.results.length).to.be.greaterThan(0); } @@ -624,21 +677,21 @@ async function workflowOrderManagement() { const { customerIds, productIds, orderModel } = await createTestData(); const orderData = { - orderNumber: 'ORD-2024-001', + orderNumber: "ORD-2024-001", customerId: customerIds[0], - status: 'pending', + status: "pending", totalAmount: 0, shippingCost: 9.99, taxAmount: 0, discountAmount: 0, finalAmount: 0, - shippingAddress: 'Via Pasta 10, Naples, 80100', - billingAddress: 'Via Pasta 10, Naples, 80100', - paymentMethod: 'credit_card', - notes: 'Express delivery requested', + shippingAddress: "Via Pasta 10, Naples, 80100", + billingAddress: "Via Pasta 10, Naples, 80100", + paymentMethod: "credit_card", + notes: "Express delivery requested", }; - const orderResponse = await newRequest('orders', orderData); + const orderResponse = await newRequest("orders", orderData); expect(orderResponse.status).to.equal(200); const orderIds = (await orderResponse.json()) as string[]; expect(orderIds).to.have.length(1); @@ -661,8 +714,8 @@ async function workflowOrderManagement() { discount: 50.0, }; - const item1Response = await newRequest('order-items', orderItem1); - const item2Response = await newRequest('order-items', orderItem2); + const item1Response = await newRequest("order-items", orderItem1); + const item2Response = await newRequest("order-items", orderItem2); expect(item1Response.status).to.equal(200); expect(item2Response.status).to.equal(200); @@ -677,19 +730,23 @@ async function workflowOrderManagement() { finalAmount, }; - const updateResponse = await editRequest('orders', orderUpdate, { id: orderIds[0] }); + const updateResponse = await editRequest("orders", orderUpdate, { + id: orderIds[0], + }); expect(updateResponse.status).to.equal(200); const updatedOrder = await orderModel.get(orderIds[0]); expect(updatedOrder?.totalAmount).to.equal(totalAmount); expect(updatedOrder?.finalAmount).to.equal(finalAmount); - const statusUpdate = { ...orderData, status: 'processing' }; - const statusResponse = await editRequest('orders', statusUpdate, { id: orderIds[0] }); + const statusUpdate = { ...orderData, status: "processing" }; + const statusResponse = await editRequest("orders", statusUpdate, { + id: orderIds[0], + }); expect(statusResponse.status).to.equal(200); const finalOrder = await orderModel.get(orderIds[0]); - expect(finalOrder?.status).to.equal('processing'); + expect(finalOrder?.status).to.equal("processing"); } async function stockManagementAndAutomaticUpdate() { @@ -699,18 +756,24 @@ async function stockManagementAndAutomaticUpdate() { const initialStock = initialProduct?.stockQuantity || 0; const stockReduction = { ...initialProduct, stockQuantity: initialStock - 3 }; - const updateResponse = await editRequest('products', stockReduction, { id: productIds[0] }); + const updateResponse = await editRequest("products", stockReduction, { + id: productIds[0], + }); expect(updateResponse.status).to.equal(200); const updatedProduct = await productModel.get(productIds[0]); expect(updatedProduct?.stockQuantity).to.equal(initialStock - 3); const invalidStock = { stockQuantity: -5 }; - const invalidResponse = await editRequest('products', invalidStock, { id: productIds[0] }); + const invalidResponse = await editRequest("products", invalidStock, { + id: productIds[0], + }); expect(invalidResponse.status).to.equal(400); const restock = { ...initialProduct, stockQuantity: initialStock + 10 }; - const restockResponse = await editRequest('products', restock, { id: productIds[0] }); + const restockResponse = await editRequest("products", restock, { + id: productIds[0], + }); expect(restockResponse.status).to.equal(200); const restockedProduct = await productModel.get(productIds[0]); @@ -720,37 +783,44 @@ async function stockManagementAndAutomaticUpdate() { async function advancedSearchAndFiltering() { await createTestData(); - const categoryResponse = await listRequest('products', { - category: 'electronics', - sortKey: 'price', - sortDirection: 'desc', + const categoryResponse = await listRequest("products", { + category: "electronics", + sortKey: "price", + sortDirection: "desc", }); expect(categoryResponse.status).to.equal(200); - const categoryData = (await categoryResponse.json()) as { results: Product[] }; + const categoryData = (await categoryResponse.json()) as { + results: Product[]; + }; expect(categoryData.results.length).to.be.greaterThan(0); - const activeCustomersResponse = await listRequest('customers', { - isActive: 'true', - sortKey: 'loyaltyPoints', - sortDirection: 'desc', + const activeCustomersResponse = await listRequest("customers", { + isActive: "true", + sortKey: "loyaltyPoints", + sortDirection: "desc", }); expect(activeCustomersResponse.status).to.equal(200); - const activeCustomersData = (await activeCustomersResponse.json()) as { results: Customer[] }; + const activeCustomersData = (await activeCustomersResponse.json()) as { + results: Customer[]; + }; expect(activeCustomersData.results.length).to.be.greaterThan(0); - const paginatedResponse = await listRequest('products', { - limit: '2', - offset: '1', + const paginatedResponse = await listRequest("products", { + limit: "2", + offset: "1", }); expect(paginatedResponse.status).to.equal(200); - const paginatedData = (await paginatedResponse.json()) as { results: Product[]; total: number }; + const paginatedData = (await paginatedResponse.json()) as { + results: Product[]; + total: number; + }; expect(paginatedData.results).to.have.length(2); expect(paginatedData.total).to.be.greaterThan(2); - const pendingOrdersResponse = await listRequest('orders', { - status: 'pending', - sortKey: 'createdAt', - sortDirection: 'desc', + const pendingOrdersResponse = await listRequest("orders", { + status: "pending", + sortKey: "createdAt", + sortDirection: "desc", }); expect(pendingOrdersResponse.status).to.equal(200); } @@ -759,25 +829,31 @@ async function clientPreferencesManagement() { const { customerIds, customerModel } = await createTestData(); const customer = await customerModel.get(customerIds[0]); - const newPreferences = ['electronics', 'gaming', 'tech']; + const newPreferences = ["electronics", "gaming", "tech"]; const preferenceUpdate = { ...customer, preferences: newPreferences }; - const updateResponse = await editRequest('customers', preferenceUpdate, { id: customerIds[0] }); + const updateResponse = await editRequest("customers", preferenceUpdate, { + id: customerIds[0], + }); expect(updateResponse.status).to.equal(200); const updatedCustomer = await customerModel.get(customerIds[0]); expect(updatedCustomer?.preferences).to.deep.equal(newPreferences); - const electronicsResponse = await listRequest('products', { - category: 'electronics', - sortKey: 'price', - sortDirection: 'asc', + const electronicsResponse = await listRequest("products", { + category: "electronics", + sortKey: "price", + sortDirection: "asc", }); expect(electronicsResponse.status).to.equal(200); - const electronicsData = (await electronicsResponse.json()) as { results: Product[] }; + const electronicsData = (await electronicsResponse.json()) as { + results: Product[]; + }; expect(electronicsData.results.length).to.be.greaterThan(0); const loyaltyUpdate = { ...updatedCustomer, loyaltyPoints: 200 }; - const loyaltyResponse = await editRequest('customers', loyaltyUpdate, { id: customerIds[0] }); + const loyaltyResponse = await editRequest("customers", loyaltyUpdate, { + id: customerIds[0], + }); expect(loyaltyResponse.status).to.equal(200); const customerWithLoyalty = await customerModel.get(customerIds[0]); @@ -786,66 +862,76 @@ async function clientPreferencesManagement() { async function errorManagementAndComplexValidation() { const invalidCustomer = { - firstName: 'Test', - lastName: 'User', - email: 'invalid-email', - phone: '123', + firstName: "Test", + lastName: "User", + email: "invalid-email", + phone: "123", isActive: true, loyaltyPoints: 0, - preferences: ['test'], + preferences: ["test"], }; - const invalidEmailResponse = await newRequest('customers', invalidCustomer); + const invalidEmailResponse = await newRequest("customers", invalidCustomer); expect(invalidEmailResponse.status).to.equal(400); const emailError = await invalidEmailResponse.text(); - expect(emailError).to.include('email'); + expect(emailError).to.include("email"); const invalidProduct = { - sku: 'TEST-001', - name: 'Test Product', + sku: "TEST-001", + name: "Test Product", price: -50.0, stockQuantity: 10, - category: 'test', + category: "test", isActive: true, }; - const invalidPriceResponse = await newRequest('products', invalidProduct); + const invalidPriceResponse = await newRequest("products", invalidProduct); expect(invalidPriceResponse.status).to.equal(400); const priceError = await invalidPriceResponse.text(); - expect(priceError).to.include('price'); + expect(priceError).to.include("price"); const invalidOrderItem = { - orderId: 'fake-order-id', - productId: 'fake-product-id', + orderId: "fake-order-id", + productId: "fake-product-id", quantity: 0, unitPrice: 10.0, totalPrice: 0, discount: 0, }; - const invalidQuantityResponse = await newRequest('order-items', invalidOrderItem); + const invalidQuantityResponse = await newRequest( + "order-items", + invalidOrderItem, + ); expect(invalidQuantityResponse.status).to.equal(400); const quantityError = await invalidQuantityResponse.text(); - expect(quantityError).to.include('quantity'); + expect(quantityError).to.include("quantity"); const { customerIds } = await createTestData(); - const invalidStatusUpdate = { status: 'invalid_status' }; - const invalidStatusResponse = await editRequest('orders', invalidStatusUpdate, { id: customerIds[0] }); + const invalidStatusUpdate = { status: "invalid_status" }; + const invalidStatusResponse = await editRequest( + "orders", + invalidStatusUpdate, + { id: customerIds[0] }, + ); expect(invalidStatusResponse.status).to.equal(400); - const invalidRouteResponse = await request('customers', 'nonexistent', 'GET'); + const invalidRouteResponse = await request("customers", "nonexistent", "GET"); expect(invalidRouteResponse.status).to.equal(404); const missingFieldsCustomer = { - firstName: 'Test', - email: 'test@example.com', + firstName: "Test", + email: "test@example.com", isActive: true, loyaltyPoints: 0, - preferences: ['test'], + preferences: ["test"], }; - const missingFieldsResponse = await newRequest('customers', missingFieldsCustomer); + const missingFieldsResponse = await newRequest( + "customers", + missingFieldsCustomer, + ); expect(missingFieldsResponse.status).to.equal(400); const missingFieldsError = await missingFieldsResponse.text(); - expect(missingFieldsError).to.include('lastName'); + expect(missingFieldsError).to.include("lastName"); } diff --git a/src/test/interfaces/data-api/beta/utils.ts b/src/test/interfaces/data-api/beta/utils.ts index 52fdf1b..25fab20 100644 --- a/src/test/interfaces/data-api/beta/utils.ts +++ b/src/test/interfaces/data-api/beta/utils.ts @@ -1,12 +1,13 @@ -import { expect } from 'chai'; -import { URL_BASE } from './constants'; +import { Schema } from "@ajs/database/beta"; +import { expect } from "chai"; +import { URL_BASE } from "./constants"; export function getFunctionName(): string { const err = new Error(); - const stack = err.stack?.split('\n'); - const line = stack?.[2] ?? ''; + const stack = err.stack?.split("\n"); + const line = stack?.[2] ?? ""; const match = line.match(/at (\w+)/); - return match?.[1] ?? 'unknown'; + return match?.[1] ?? "unknown"; } export async function request( @@ -16,41 +17,77 @@ export async function request( payload?: unknown, queryParams?: Record, ) { - return await fetch(`${URL_BASE}/${functionName}/${uri}?${new URLSearchParams(queryParams).toString()}`, { - method, - headers: { - 'Content-Type': 'application/json', + return await fetch( + `${URL_BASE}/${functionName}/${uri}?${new URLSearchParams(queryParams).toString()}`, + { + method, + headers: { + "Content-Type": "application/json", + }, + body: payload ? JSON.stringify(payload) : undefined, }, - body: payload ? JSON.stringify(payload) : undefined, - }); + ); } -export async function newRequest(functionName: string, payload: unknown, queryParams?: Record) { - return await request(functionName, 'new', 'POST', payload, queryParams); +export async function newRequest( + functionName: string, + payload: unknown, + queryParams?: Record, +) { + return await request(functionName, "new", "POST", payload, queryParams); } -export async function getRequest(functionName: string, queryParams?: Record) { - return await request(functionName, 'get', 'GET', undefined, queryParams); +export async function getRequest( + functionName: string, + queryParams?: Record, +) { + return await request(functionName, "get", "GET", undefined, queryParams); } -export async function listRequest(functionName: string, queryParams?: Record) { - return await request(functionName, 'list', 'GET', undefined, queryParams); +export async function listRequest( + functionName: string, + queryParams?: Record, +) { + return await request(functionName, "list", "GET", undefined, queryParams); } -export async function editRequest(functionName: string, payload: unknown, queryParams?: Record) { - return await request(functionName, 'edit', 'PUT', payload, queryParams); +export async function editRequest( + functionName: string, + payload: unknown, + queryParams?: Record, +) { + return await request(functionName, "edit", "PUT", payload, queryParams); } -export async function deleteRequest(functionName: string, queryParams?: Record) { - return await request(functionName, 'delete', 'DELETE', undefined, queryParams); +export async function deleteRequest( + functionName: string, + queryParams?: Record, +) { + return await request( + functionName, + "delete", + "DELETE", + undefined, + queryParams, + ); } -export async function validateObject(object: T, expectedObject: Partial, fieldsToCheck: (keyof T)[]) { +export async function validateObject( + object: T, + expectedObject: Partial, + fieldsToCheck: (keyof T)[], +) { for (const field of fieldsToCheck) { expect(object[field]).to.deep.equal(expectedObject[field]); } } +export function getSchemaInstance(schemaName: string) { + const schema = Schema.get(schemaName); + if (!schema) throw new Error(`Schema "${schemaName}" not found`); + return schema.instance(); +} + export async function validateObjectList( objectList: T[], expectedObjectList: Partial[], From 2370d361e453c7bc78e34a268ad2d3613d3a47e7 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sat, 7 Mar 2026 17:44:36 +0100 Subject: [PATCH 2/2] refactor: migrate from ESLint and Prettier to Biome Replace ESLint and Prettier with Biome for linting and formatting. Remove eslint.config.mjs, .prettierrc.json, .prettierignore and related devDependencies. Add biome.json configuration and reformat codebase to match Biome's style (double quotes, sorted imports, space indentation). --- .editorconfig | 4 +- .prettierignore | 10 - .prettierrc.json | 11 -- biome.json | 53 +++++ eslint.config.mjs | 101 ---------- output/data-api/beta/components.d.ts | 12 +- output/data-api/beta/index.d.ts | 26 +-- output/data-api/beta/metadata.d.ts | 22 ++- package.json | 18 +- playground/src/data-api/user.ts | 36 +++- playground/src/db/user.ts | 173 +++++++++++++--- playground/src/index.ts | 10 +- playground/src/utils.ts | 2 +- playground/tsconfig.json | 6 +- pnpm-lock.yaml | 118 ++++++++--- src/interfaces/data-api/beta/index.ts | 184 +++++++++++++----- src/test/antelope.test.js | 40 ++-- .../interfaces/data-api/beta/constants.ts | 2 +- 18 files changed, 522 insertions(+), 306 deletions(-) delete mode 100644 .prettierignore delete mode 100644 .prettierrc.json create mode 100644 biome.json delete mode 100644 eslint.config.mjs diff --git a/.editorconfig b/.editorconfig index 5ebad2c..5d12634 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,8 +2,8 @@ root = true [*] -indent_style = tab -indent_size = 4 +indent_style = space +indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 08848c5..0000000 --- a/.prettierignore +++ /dev/null @@ -1,10 +0,0 @@ -node_modules -tmp -dist -.antelope -output -coverage -.git -*.log -**/*.min.js -pnpm-lock.yaml \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index e4556c9..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "semi": true, - "singleQuote": true, - "trailingComma": "all", - "printWidth": 120, - "tabWidth": 2, - "useTabs": false, - "bracketSpacing": true, - "arrowParens": "always", - "endOfLine": "lf" -} diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..f3a605f --- /dev/null +++ b/biome.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.2/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "includes": [ + "**", + "!dist", + "!**/dist", + "!node_modules", + "!**/node_modules", + "!.antelope", + "!output" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "off" + }, + "nursery": { + "noFloatingPromises": "error" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + }, + "parser": { + "unsafeParameterDecoratorsEnabled": true + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 9892d72..0000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,101 +0,0 @@ -import eslint from '@eslint/js'; -import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; -import eslintPluginImport from 'eslint-plugin-import'; -import globals from 'globals'; -import tseslint from 'typescript-eslint'; - -export default tseslint.config( - { - ignores: [ - 'node_modules', - 'tmp', - 'dist', - '.antelope', - 'output', - 'coverage', - '.git', - '*.log', - '**/*.min.js', - 'pnpm-lock.yaml', - 'eslint.config.mjs', - 'playground', - 'src/test/antelope.test.js', - ], - }, - eslint.configs.recommended, - ...tseslint.configs.recommendedTypeChecked, - eslintPluginPrettierRecommended, - eslintPluginImport.flatConfigs.recommended, - eslintPluginImport.flatConfigs.typescript, - { - languageOptions: { - globals: { - ...globals.node, - ...globals.jest, - }, - ecmaVersion: 12, - sourceType: 'module', - parserOptions: { - project: ['tsconfig.json'], - projectService: { - allowDefaultProject: ['*.ts', 'eslint.config.mjs'], - }, - tsconfigRootDir: import.meta.dirname, - }, - }, - }, - { - rules: { - 'no-console': 'warn', - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'warn', - //'@typescript-eslint/no-floating-promises': 'error', - //'@typescript-eslint/no-misused-promises': 'error', - '@typescript-eslint/no-throw-literal': 'off', - '@typescript-eslint/no-unnecessary-type-assertion': 'warn', - '@typescript-eslint/no-unsafe-assignment': 'warn', - '@typescript-eslint/restrict-template-expressions': 'warn', - '@typescript-eslint/no-namespace': 'off', - '@typescript-eslint/no-unsafe-member-access': 'warn', - '@typescript-eslint/only-throw-error': 'off', - '@typescript-eslint/prefer-promise-reject-errors': 'off', - '@typescript-eslint/no-unsafe-call': 'off', - '@typescript-eslint/no-unsafe-member-access': 'off', - 'import/prefer-default-export': 'off', - 'import/no-extraneous-dependencies': ['error', { devDependencies: ['**/*.test.ts', '**/*.spec.ts'] }], - 'import/extensions': [ - 'error', - 'ignorePackages', - { - js: 'never', - jsx: 'never', - ts: 'never', - tsx: 'never', - }, - ], - 'max-len': ['warn', { code: 120 }], - quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }], - 'import/no-unresolved': ['error', { ignore: ['^@ajs(/.*)?$', '^@ajs\\.local(/.*)?$', 'typescript\\-eslint'] }], - - // TODO: fix these rules - '@typescript-eslint/unbound-method': 'off', - '@typescript-eslint/no-unsafe-return': 'off', - '@typescript-eslint/no-unsafe-argument': 'off', - '@typescript-eslint/require-await': 'off', - '@typescript-eslint/no-implied-eval': 'off', - '@typescript-eslint/no-floating-promises': 'off', - '@typescript-eslint/no-misused-promises': 'off', - '@typescript-eslint/no-require-imports': 'off', - '@typescript-eslint/prefer-promise-reject-errors': 'off', - '@typescript-eslint/no-redundant-type-constituents': 'off', - 'no-useless-escape': 'off', - '@typescript-eslint/no-empty-object-type': 'off', - '@typescript-eslint/await-thenable': 'off', - 'prefer-const': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - }, - }, -); diff --git a/output/data-api/beta/components.d.ts b/output/data-api/beta/components.d.ts index ee801ac..60b69a5 100644 --- a/output/data-api/beta/components.d.ts +++ b/output/data-api/beta/components.d.ts @@ -1,7 +1,7 @@ -import { RequestContext } from '@ajs/api/beta'; -import { Datum, Stream, Table, ValueProxy, SchemaInstance } from '@ajs/database/beta'; -import { DataModel } from '@ajs/database-decorators/beta/model'; -import { DataAPIMeta, FilterValue } from './metadata'; +import { type RequestContext } from "@ajs/api/beta"; +import { type Datum, type SchemaInstance, Stream, type Table, type ValueProxy } from "@ajs/database/beta"; +import type { DataModel } from "@ajs/database-decorators/beta/model"; +import type { DataAPIMeta, FilterValue } from "./metadata"; export declare namespace Parameters { export function GetOptionOverrides>(reqCtx: RequestContext): T; export function ExtractFilters(reqCtx: RequestContext, meta: DataAPIMeta): Record; @@ -21,7 +21,7 @@ export declare namespace Parameters { offset?: number; limit?: number; sortKey?: string; - sortDirection?: 'asc' | 'desc'; + sortDirection?: "asc" | "desc"; maxPage?: number; noForeign?: boolean; noPluck?: boolean; @@ -59,7 +59,7 @@ export declare namespace Query { function ReadProperties(obj: any, meta: DataAPIMeta, dbData: any, action?: string, onlyList?: boolean): Promise>; function WriteProperties(obj: any, meta: DataAPIMeta, bodyData: Record, action?: string, existingDBData?: Record): Promise>; function Get(table: Table, id: string | ValueProxy, index?: string): Datum; - function List>(obj: any, meta: DataAPIMeta, request: Table, reqCtx: RequestContext, sorting?: [string, 'asc' | 'desc' | undefined], filters?: Record): [sorted: Stream, total: Datum]; + function List>(obj: any, meta: DataAPIMeta, request: Table, reqCtx: RequestContext, sorting?: [string, "asc" | "desc" | undefined], filters?: Record): [sorted: Stream, total: Datum]; function Delete(table: Table, id: string | string[]): import("@ajs/database/beta").Query; } export declare namespace Validation { diff --git a/output/data-api/beta/index.d.ts b/output/data-api/beta/index.d.ts index 9b68f88..e13befe 100644 --- a/output/data-api/beta/index.d.ts +++ b/output/data-api/beta/index.d.ts @@ -1,7 +1,7 @@ -import { Class, ParameterDecorator } from '@ajs/core/beta/decorators'; -import { RequestContext, ControllerClass } from '@ajs/api/beta'; -import { DataAPIMeta } from './metadata'; -import { Parameters } from './components'; +import { type ControllerClass, type RequestContext } from "@ajs/api/beta"; +import { type Class, type ParameterDecorator } from "@ajs/core/beta/decorators"; +import { Parameters } from "./components"; +import { DataAPIMeta } from "./metadata"; export type DataControllerCallback = { args: (ParameterDecorator | ParameterDecorator[])[]; method: string; @@ -12,7 +12,7 @@ export type DataControllerCallbackWithOptions = { options?: Partial; callback: DataControllerCallback; }; -export type ExtractCallback = T extends DataControllerCallbackWithOptions ? T['callback']['func'] : T extends DataControllerCallback ? T['func'] : never; +export type ExtractCallback = T extends DataControllerCallbackWithOptions ? T["callback"]["func"] : T extends DataControllerCallback ? T["func"] : never; export type DataControllerDef = { [name: string]: DataControllerCallback | DataControllerCallbackWithOptions; }; @@ -27,7 +27,7 @@ export declare const RegisterDataController: () => import("@ajs/core/beta/decora export declare function GetDataControllerMeta(thisObj: any): DataAPIMeta; export declare namespace DefaultRoutes { const Get: { - func: (reqCtx: RequestContext, params: Parameters.GetParameters) => Promise>; + func: (_reqCtx: RequestContext, params: Parameters.GetParameters) => Promise>; args: (import("@ajs/core/beta/decorators").PropertyDecorator & ParameterDecorator)[]; method: string; }; @@ -42,23 +42,23 @@ export declare namespace DefaultRoutes { method: string; }; const New: { - func: (reqCtx: RequestContext, params: Parameters.NewParameters, body: Buffer) => Promise; + func: (_reqCtx: RequestContext, params: Parameters.NewParameters, body: Buffer) => Promise; args: (import("@ajs/core/beta/decorators").PropertyDecorator & ParameterDecorator)[]; method: string; }; const Edit: { - func: (reqCtx: RequestContext, params: Parameters.EditParameters, body: Buffer) => Promise; + func: (_reqCtx: RequestContext, params: Parameters.EditParameters, body: Buffer) => Promise; args: (import("@ajs/core/beta/decorators").PropertyDecorator & ParameterDecorator)[]; method: string; }; const Delete: { - func: (reqCtx: RequestContext, params: Parameters.DeleteParameters) => Promise; + func: (_reqCtx: RequestContext, params: Parameters.DeleteParameters) => Promise; args: (import("@ajs/core/beta/decorators").PropertyDecorator & ParameterDecorator)[]; method: string; }; const All: { readonly get: { - func: (reqCtx: RequestContext, params: Parameters.GetParameters) => Promise>; + func: (_reqCtx: RequestContext, params: Parameters.GetParameters) => Promise>; args: (import("@ajs/core/beta/decorators").PropertyDecorator & ParameterDecorator)[]; method: string; }; @@ -73,17 +73,17 @@ export declare namespace DefaultRoutes { method: string; }; readonly new: { - func: (reqCtx: RequestContext, params: Parameters.NewParameters, body: Buffer) => Promise; + func: (_reqCtx: RequestContext, params: Parameters.NewParameters, body: Buffer) => Promise; args: (import("@ajs/core/beta/decorators").PropertyDecorator & ParameterDecorator)[]; method: string; }; readonly edit: { - func: (reqCtx: RequestContext, params: Parameters.EditParameters, body: Buffer) => Promise; + func: (_reqCtx: RequestContext, params: Parameters.EditParameters, body: Buffer) => Promise; args: (import("@ajs/core/beta/decorators").PropertyDecorator & ParameterDecorator)[]; method: string; }; readonly delete: { - func: (reqCtx: RequestContext, params: Parameters.DeleteParameters) => Promise; + func: (_reqCtx: RequestContext, params: Parameters.DeleteParameters) => Promise; args: (import("@ajs/core/beta/decorators").PropertyDecorator & ParameterDecorator)[]; method: string; }; diff --git a/output/data-api/beta/metadata.d.ts b/output/data-api/beta/metadata.d.ts index df861e1..4b7967f 100644 --- a/output/data-api/beta/metadata.d.ts +++ b/output/data-api/beta/metadata.d.ts @@ -1,9 +1,9 @@ -import { Class } from '@ajs/core/beta/decorators'; -import { RequestContext } from '@ajs/api/beta'; -import { ValueProxy, ValueProxyOrValue } from '@ajs/database/beta'; -import { DataControllerCallbackWithOptions } from '.'; -import { ContainerModifier } from '@ajs/database-decorators/beta/modifiers/common'; -import { Table } from '@ajs/database-decorators/beta'; +import type { RequestContext } from "@ajs/api/beta"; +import { type Class } from "@ajs/core/beta/decorators"; +import type { ValueProxy, ValueProxyOrValue } from "@ajs/database/beta"; +import { type Table } from "@ajs/database-decorators/beta"; +import type { ContainerModifier } from "@ajs/database-decorators/beta/modifiers/common"; +import type { DataControllerCallbackWithOptions } from "."; /** * Field access mode enum. */ @@ -47,7 +47,13 @@ export interface FieldData { /** * Foreign key reference. */ - foreign?: [table: string, tableClass?: Class
, index?: string, multi?: true, pluck?: string[]]; + foreign?: [ + table: string, + tableClass?: Class
, + index?: string, + multi?: true, + pluck?: string[] + ]; /** * Value validator callback. */ @@ -61,7 +67,7 @@ export interface FieldData { */ indexable?: boolean; } -type Comparison = 'eq' | 'ne' | 'gt' | 'ge' | 'lt' | 'le'; +type Comparison = "eq" | "ne" | "gt" | "ge" | "lt" | "le"; export type FilterValue = [value: string, mode: Comparison]; /** * Filter callback. diff --git a/package.json b/package.json index ddc6096..0f3284d 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,10 @@ "build": "rimraf dist && tsc", "dev:prepare": "cd playground && ajs module imports install", "dev": "ajs project run -w -p playground", - "format": "prettier --write .", + "format": "biome format --write .", "generate": "ajs module exports generate", - "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", - "lint": "eslint . --ext .js,.jsx,.ts,.tsx", + "lint:fix": "biome check --write .", + "lint": "biome check .", "typecheck": "tsc --noEmit", "prepack": "pnpm run build", "prepare": "ajs module imports install", @@ -57,22 +57,14 @@ "reflect-metadata": "^0.2.2" }, "devDependencies": { - "@eslint/eslintrc": "^3.3.1", + "@biomejs/biome": "2.3.2", "rimraf": "^6.0.1", - "@eslint/js": "^9.25.0", "@types/chai": "^4.3.3", "@types/mocha": "^10.0.10", "@types/node": "^22.14.1", - "eslint": "^9.25.0", - "eslint-config-prettier": "^10.1.2", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-prettier": "^5.2.6", - "globals": "^16.0.0", "mongodb-memory-server-core": "^10.1.4", - "prettier": "^3.5.3", "release-it": "^19.0.2", "release-it-changelogen": "^0.1.0", - "typescript": "^5.8.3", - "typescript-eslint": "^8.30.1" + "typescript": "^5.8.3" } } diff --git a/playground/src/data-api/user.ts b/playground/src/data-api/user.ts index 3df227d..f7eb6de 100644 --- a/playground/src/data-api/user.ts +++ b/playground/src/data-api/user.ts @@ -1,8 +1,20 @@ -import { DataController, DefaultRoutes, RegisterDataController } from '@ajs/data-api/beta'; -import { User, UserModel } from '../db/user'; -import { Controller } from '@ajs/api/beta'; -import { Model } from '@ajs/database-decorators/beta'; -import { Access, AccessMode, Listable, Mandatory, ModelReference, Sortable } from '@ajs/data-api/beta/metadata'; +import { Controller } from "@ajs/api/beta"; +import { + DataController, + DefaultRoutes, + RegisterDataController, +} from "@ajs/data-api/beta"; +import { + Access, + AccessMode, + Listable, + Mandatory, + ModelReference, + Sortable, +} from "@ajs/data-api/beta/metadata"; +import { Model } from "@ajs/database-decorators/beta"; +import { User, UserModel } from "../db/user"; + const routes = { get: DefaultRoutes.Get, list: DefaultRoutes.List, @@ -12,9 +24,13 @@ const routes = { }; @RegisterDataController() -export class UserDataAPI extends DataController(User, routes, Controller('/users')) { +export class UserDataAPI extends DataController( + User, + routes, + Controller("/users"), +) { @ModelReference() - @Model(UserModel, 'default') + @Model(UserModel, "default") declare userModel: UserModel; @Listable() @@ -24,19 +40,19 @@ export class UserDataAPI extends DataController(User, routes, Controller('/users @Listable() @Sortable() - @Mandatory('new', 'edit') + @Mandatory("new", "edit") @Access(AccessMode.ReadWrite) declare email: string; @Listable() @Sortable() - @Mandatory('new', 'edit') + @Mandatory("new", "edit") @Access(AccessMode.ReadWrite) declare firstName: string; @Listable() @Sortable() - @Mandatory('new', 'edit') + @Mandatory("new", "edit") @Access(AccessMode.ReadWrite) declare lastName: string; } diff --git a/playground/src/db/user.ts b/playground/src/db/user.ts index ca65d35..ed359ad 100644 --- a/playground/src/db/user.ts +++ b/playground/src/db/user.ts @@ -1,32 +1,153 @@ -import { BasicDataModel, Index, Table, RegisterTable, Fixture } from '@ajs/database-decorators/beta'; -import { schemaName } from '../utils'; +import { + BasicDataModel, + Fixture, + Index, + RegisterTable, + Table, +} from "@ajs/database-decorators/beta"; +import { schemaName } from "../utils"; -const tableName = 'users'; +const tableName = "users"; @Fixture(() => [ - { _id: 'admin', email: 'admin@example.com', firstName: 'Admin', lastName: 'User' }, - { _id: 'user', email: 'user@example.com', firstName: 'Standard', lastName: 'User' }, - { _id: 'guest', email: 'guest@example.com', firstName: 'Guest', lastName: 'User' }, - { _id: 'user1', email: 'user1@example.com', firstName: 'John', lastName: 'Doe' }, - { _id: 'user2', email: 'user2@example.com', firstName: 'Jane', lastName: 'Smith' }, - { _id: 'user3', email: 'user3@example.com', firstName: 'Robert', lastName: 'Johnson' }, - { _id: 'user4', email: 'user4@example.com', firstName: 'Emily', lastName: 'Williams' }, - { _id: 'user5', email: 'user5@example.com', firstName: 'Michael', lastName: 'Brown' }, - { _id: 'user6', email: 'user6@example.com', firstName: 'Sarah', lastName: 'Jones' }, - { _id: 'user7', email: 'user7@example.com', firstName: 'David', lastName: 'Garcia' }, - { _id: 'user8', email: 'user8@example.com', firstName: 'Lisa', lastName: 'Miller' }, - { _id: 'user9', email: 'user9@example.com', firstName: 'Thomas', lastName: 'Davis' }, - { _id: 'user10', email: 'user10@example.com', firstName: 'Jennifer', lastName: 'Martinez' }, - { _id: 'user11', email: 'user11@example.com', firstName: 'Christopher', lastName: 'Rodriguez' }, - { _id: 'user12', email: 'user12@example.com', firstName: 'Michelle', lastName: 'Wilson' }, - { _id: 'user13', email: 'user13@example.com', firstName: 'Daniel', lastName: 'Anderson' }, - { _id: 'user14', email: 'user14@example.com', firstName: 'Jessica', lastName: 'Taylor' }, - { _id: 'user15', email: 'user15@example.com', firstName: 'James', lastName: 'Thomas' }, - { _id: 'user16', email: 'user16@example.com', firstName: 'Elizabeth', lastName: 'Moore' }, - { _id: 'user17', email: 'user17@example.com', firstName: 'Matthew', lastName: 'Jackson' }, - { _id: 'user18', email: 'user18@example.com', firstName: 'Nicole', lastName: 'White' }, - { _id: 'user19', email: 'user19@example.com', firstName: 'Andrew', lastName: 'Harris' }, - { _id: 'user20', email: 'user20@example.com', firstName: 'Stephanie', lastName: 'Clark' }, + { + _id: "admin", + email: "admin@example.com", + firstName: "Admin", + lastName: "User", + }, + { + _id: "user", + email: "user@example.com", + firstName: "Standard", + lastName: "User", + }, + { + _id: "guest", + email: "guest@example.com", + firstName: "Guest", + lastName: "User", + }, + { + _id: "user1", + email: "user1@example.com", + firstName: "John", + lastName: "Doe", + }, + { + _id: "user2", + email: "user2@example.com", + firstName: "Jane", + lastName: "Smith", + }, + { + _id: "user3", + email: "user3@example.com", + firstName: "Robert", + lastName: "Johnson", + }, + { + _id: "user4", + email: "user4@example.com", + firstName: "Emily", + lastName: "Williams", + }, + { + _id: "user5", + email: "user5@example.com", + firstName: "Michael", + lastName: "Brown", + }, + { + _id: "user6", + email: "user6@example.com", + firstName: "Sarah", + lastName: "Jones", + }, + { + _id: "user7", + email: "user7@example.com", + firstName: "David", + lastName: "Garcia", + }, + { + _id: "user8", + email: "user8@example.com", + firstName: "Lisa", + lastName: "Miller", + }, + { + _id: "user9", + email: "user9@example.com", + firstName: "Thomas", + lastName: "Davis", + }, + { + _id: "user10", + email: "user10@example.com", + firstName: "Jennifer", + lastName: "Martinez", + }, + { + _id: "user11", + email: "user11@example.com", + firstName: "Christopher", + lastName: "Rodriguez", + }, + { + _id: "user12", + email: "user12@example.com", + firstName: "Michelle", + lastName: "Wilson", + }, + { + _id: "user13", + email: "user13@example.com", + firstName: "Daniel", + lastName: "Anderson", + }, + { + _id: "user14", + email: "user14@example.com", + firstName: "Jessica", + lastName: "Taylor", + }, + { + _id: "user15", + email: "user15@example.com", + firstName: "James", + lastName: "Thomas", + }, + { + _id: "user16", + email: "user16@example.com", + firstName: "Elizabeth", + lastName: "Moore", + }, + { + _id: "user17", + email: "user17@example.com", + firstName: "Matthew", + lastName: "Jackson", + }, + { + _id: "user18", + email: "user18@example.com", + firstName: "Nicole", + lastName: "White", + }, + { + _id: "user19", + email: "user19@example.com", + firstName: "Andrew", + lastName: "Harris", + }, + { + _id: "user20", + email: "user20@example.com", + firstName: "Stephanie", + lastName: "Clark", + }, ]) @RegisterTable(tableName, schemaName) export class User extends Table { diff --git a/playground/src/index.ts b/playground/src/index.ts index 8ecc0f4..4e34d1f 100644 --- a/playground/src/index.ts +++ b/playground/src/index.ts @@ -1,12 +1,12 @@ -import { CreateDatabaseSchemaInstance } from '@ajs/database-decorators/beta'; -import { schemaName } from './utils'; -import './db/user'; -import './data-api/user'; +import { CreateDatabaseSchemaInstance } from "@ajs/database-decorators/beta"; +import { schemaName } from "./utils"; +import "./db/user"; +import "./data-api/user"; export function construct(): void {} export async function start(): Promise { - await CreateDatabaseSchemaInstance(schemaName, 'default'); + await CreateDatabaseSchemaInstance(schemaName, "default"); } export function destroy(): void {} diff --git a/playground/src/utils.ts b/playground/src/utils.ts index 02b5015..a30ba2f 100644 --- a/playground/src/utils.ts +++ b/playground/src/utils.ts @@ -1 +1 @@ -export const schemaName = 'data-api-playground'; +export const schemaName = "data-api-playground"; diff --git a/playground/tsconfig.json b/playground/tsconfig.json index 08b439f..17a0449 100644 --- a/playground/tsconfig.json +++ b/playground/tsconfig.json @@ -19,7 +19,11 @@ "useDefineForClassFields": true, "baseUrl": "src", "paths": { - "@ajs/*": ["../../output/*", "../../.antelope/interfaces.d/*", "../.antelope/interfaces.d/*"] + "@ajs/*": [ + "../../output/*", + "../../.antelope/interfaces.d/*", + "../.antelope/interfaces.d/*" + ] } }, "include": ["src/**/*.ts"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5091fff..5a35301 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,12 +18,9 @@ importers: specifier: ^0.2.2 version: 0.2.2 devDependencies: - '@eslint/eslintrc': - specifier: ^3.3.1 - version: 3.3.1 - '@eslint/js': - specifier: ^9.25.0 - version: 9.25.1 + '@biomejs/biome': + specifier: 2.3.2 + version: 2.3.2 '@types/chai': specifier: ^4.3.3 version: 4.3.20 @@ -33,27 +30,9 @@ importers: '@types/node': specifier: ^22.14.1 version: 22.14.1 - eslint: - specifier: ^9.25.0 - version: 9.25.1(jiti@2.5.1) - eslint-config-prettier: - specifier: ^10.1.2 - version: 10.1.2(eslint@9.25.1(jiti@2.5.1)) - eslint-plugin-import: - specifier: ^2.31.0 - version: 2.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.5.1))(typescript@5.8.3))(eslint@9.25.1(jiti@2.5.1)) - eslint-plugin-prettier: - specifier: ^5.2.6 - version: 5.2.6(eslint-config-prettier@10.1.2(eslint@9.25.1(jiti@2.5.1)))(eslint@9.25.1(jiti@2.5.1))(prettier@3.5.3) - globals: - specifier: ^16.0.0 - version: 16.0.0 mongodb-memory-server-core: specifier: ^10.1.4 version: 10.2.0(socks@2.8.4) - prettier: - specifier: ^3.5.3 - version: 3.5.3 release-it: specifier: ^19.0.2 version: 19.0.2(@types/node@22.14.1) @@ -66,9 +45,6 @@ importers: typescript: specifier: ^5.8.3 version: 5.8.3 - typescript-eslint: - specifier: ^8.30.1 - version: 8.31.0(eslint@9.25.1(jiti@2.5.1))(typescript@5.8.3) playground: dependencies: @@ -100,6 +76,59 @@ importers: packages: + '@biomejs/biome@2.3.2': + resolution: {integrity: sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.3.2': + resolution: {integrity: sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.3.2': + resolution: {integrity: sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.3.2': + resolution: {integrity: sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.3.2': + resolution: {integrity: sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.3.2': + resolution: {integrity: sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.3.2': + resolution: {integrity: sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@2.3.2': + resolution: {integrity: sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.3.2': + resolution: {integrity: sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.5.1': resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2360,6 +2389,41 @@ packages: snapshots: + '@biomejs/biome@2.3.2': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.3.2 + '@biomejs/cli-darwin-x64': 2.3.2 + '@biomejs/cli-linux-arm64': 2.3.2 + '@biomejs/cli-linux-arm64-musl': 2.3.2 + '@biomejs/cli-linux-x64': 2.3.2 + '@biomejs/cli-linux-x64-musl': 2.3.2 + '@biomejs/cli-win32-arm64': 2.3.2 + '@biomejs/cli-win32-x64': 2.3.2 + + '@biomejs/cli-darwin-arm64@2.3.2': + optional: true + + '@biomejs/cli-darwin-x64@2.3.2': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.3.2': + optional: true + + '@biomejs/cli-linux-arm64@2.3.2': + optional: true + + '@biomejs/cli-linux-x64-musl@2.3.2': + optional: true + + '@biomejs/cli-linux-x64@2.3.2': + optional: true + + '@biomejs/cli-win32-arm64@2.3.2': + optional: true + + '@biomejs/cli-win32-x64@2.3.2': + optional: true + '@eslint-community/eslint-utils@4.5.1(eslint@9.25.1(jiti@2.5.1))': dependencies: eslint: 9.25.1(jiti@2.5.1) diff --git a/src/interfaces/data-api/beta/index.ts b/src/interfaces/data-api/beta/index.ts index b28faeb..b02f99a 100644 --- a/src/interfaces/data-api/beta/index.ts +++ b/src/interfaces/data-api/beta/index.ts @@ -1,14 +1,28 @@ -import { GetMetadata } from '@ajs/core/beta'; -import { Class, MakeClassDecorator, ParameterDecorator } from '@ajs/core/beta/decorators'; -import { Route, RawBody, RequestContext, Context, ControllerClass, RegisterRoute, ControllerMeta } from '@ajs/api/beta'; -import { Datum } from '@ajs/database/beta'; -import { getTablesForSchema } from '@ajs/database-decorators/beta/schema'; -import { getMetadata, DatumStaticMetadata } from '@ajs/database-decorators/beta'; -import { assert } from '@ajs/api-util/beta'; -import { DataAPIMeta } from './metadata'; -import { Parameters, Query, Validation } from './components'; -import assert_ from 'assert'; -import { triggerEvent } from '@ajs/database-decorators/beta/modifiers/common'; +import assert_ from "node:assert"; +import { + Context, + type ControllerClass, + ControllerMeta, + RawBody, + RegisterRoute, + type RequestContext, + Route, +} from "@ajs/api/beta"; +import { assert } from "@ajs/api-util/beta"; +import { GetMetadata } from "@ajs/core/beta"; +import { + type Class, + MakeClassDecorator, + type ParameterDecorator, +} from "@ajs/core/beta/decorators"; +import { + DatumStaticMetadata, + getMetadata, +} from "@ajs/database-decorators/beta"; +import { triggerEvent } from "@ajs/database-decorators/beta/modifiers/common"; +import { getTablesForSchema } from "@ajs/database-decorators/beta/schema"; +import { Parameters, Query, Validation } from "./components"; +import { DataAPIMeta } from "./metadata"; export type DataControllerCallback = { args: (ParameterDecorator | ParameterDecorator[])[]; @@ -23,9 +37,9 @@ export type DataControllerCallbackWithOptions = { }; export type ExtractCallback = T extends DataControllerCallbackWithOptions - ? T['callback']['func'] + ? T["callback"]["func"] : T extends DataControllerCallback - ? T['func'] + ? T["func"] : never; export type DataControllerDef = { @@ -44,25 +58,34 @@ export function DataController< C extends Class, P extends DataControllerDef = DataControllerDef, Base extends ControllerClass = ControllerClass, ->(tableClass: C, def: P, base: Base): Class & TableHolder> & Base { +>( + tableClass: C, + def: P, + base: Base, +): Class & TableHolder> & Base { const c = class extends base { table!: InstanceType; }; const meta = GetMetadata(c, DataAPIMeta); const tableMetadata = getMetadata(tableClass, DatumStaticMetadata); - meta.schemaName = tableMetadata.schemaName || 'default'; + meta.schemaName = tableMetadata.schemaName || "default"; const databaseSchema = getTablesForSchema(meta.schemaName); - assert_(databaseSchema, 'Non-existent Database Schema'); + assert_(databaseSchema, "Non-existent Database Schema"); - const tableName = Object.entries(databaseSchema).find(([, table]) => table === tableClass)?.[0]; - assert_(tableName, 'Unregistered Database Table'); + const tableName = Object.entries(databaseSchema).find( + ([, table]) => table === tableClass, + )?.[0]; + assert_(tableName, "Unregistered Database Table"); meta.tableClass = tableClass; meta.tableName = tableName; for (const [key, val] of Object.entries(def)) { - const entry = 'func' in val ? { endpoint: key, callback: val } : { endpoint: key, ...val }; + const entry = + "func" in val + ? { endpoint: key, callback: val } + : { endpoint: key, ...val }; meta.addEndpoint(key, entry); // TODO?: should this go in addEndpoint? c.prototype[key] = entry.callback.func; @@ -76,37 +99,50 @@ export const RegisterDataController = MakeClassDecorator((target) => { for (let i = 0; i < entry.callback.args.length; ++i) { const arg = entry.callback.args[i]; if (Array.isArray(arg)) { - arg.forEach((step) => step(target.prototype, key, i)); + for (const step of arg) { + step(target.prototype, key, i); + } } else { arg(target.prototype, key, i); } } const meta = GetMetadata(target, ControllerMeta); - const fullLocation = `${meta.location}/${entry.endpoint ?? key}`.replace(/\/+/g, '/'); + const fullLocation = `${meta.location}/${entry.endpoint ?? key}`.replace( + /\/+/g, + "/", + ); RegisterRoute({ callback: (ctx) => { ctx.dataAPIEntry = entry; }, location: fullLocation, method: entry.callback.method, - mode: 'prefix', + mode: "prefix", parameters: [{ provider: (ctx) => ctx, modifiers: [] }], properties: meta.computed_props, proto: target.prototype, }); - Route('handler', entry.callback.method, entry.endpoint)(target.prototype, key, { - value: entry.callback.func, - }); + Route("handler", entry.callback.method, entry.endpoint)( + target.prototype, + key, + { + value: entry.callback.func, + }, + ); } }); export function GetDataControllerMeta(thisObj: any): DataAPIMeta { - return GetMetadata(Object.getPrototypeOf(thisObj).constructor, DataAPIMeta, true); + return GetMetadata( + Object.getPrototypeOf(thisObj).constructor, + DataAPIMeta, + true, + ); } export namespace DefaultRoutes { class Methods { - async get(reqCtx: RequestContext, params: Parameters.GetParameters) { + async get(_reqCtx: RequestContext, params: Parameters.GetParameters) { const meta = GetDataControllerMeta(this); const model = Query.GetModel(this, meta); @@ -117,10 +153,10 @@ export namespace DefaultRoutes { } const dbResult = model.constructor.fromDatabase(await query); - assert(dbResult, 404, 'Not Found'); + assert(dbResult, 404, "Not Found"); Validation.Unlock(this, meta, dbResult); - const results = await Query.ReadProperties(this, meta, dbResult, 'get'); + const results = await Query.ReadProperties(this, meta, dbResult, "get"); Validation.ClearInternal(meta, results); @@ -132,15 +168,35 @@ export namespace DefaultRoutes { const model = Query.GetModel(this, meta); const sort = params?.sortKey - ? ([params.sortKey, params.sortDirection] as [string, 'asc' | 'desc' | undefined]) + ? ([params.sortKey, params.sortDirection] as [ + string, + "asc" | "desc" | undefined, + ]) : undefined; - let [query, queryTotal] = Query.List(this, meta, model.table, reqCtx, sort, params?.filters); + let [query, queryTotal] = Query.List( + this, + meta, + model.table, + reqCtx, + sort, + params?.filters, + ); - const pluck: Set | undefined = meta.pluck[params.pluckMode ?? 'list']; - assert(params.noPluck || pluck, 400, `No fields found for pluckMode '${params.pluckMode ?? 'list'}'`); + const pluck: Set | undefined = + meta.pluck[params.pluckMode ?? "list"]; + assert( + params.noPluck || pluck, + 400, + `No fields found for pluckMode '${params.pluckMode ?? "list"}'`, + ); if (!params.noForeign) { - query = Query.Foreign(model.database, meta, query, params.noPluck ? undefined : pluck); + query = Query.Foreign( + model.database, + meta, + query, + params.noPluck ? undefined : pluck, + ); } const offset = params.offset || 0; @@ -148,7 +204,7 @@ export namespace DefaultRoutes { let queryPaged = query.slice(offset, limit); if (!params.noPluck && pluck) { - queryPaged = queryPaged.pluck('_internal', ...pluck); + queryPaged = queryPaged.pluck("_internal", ...pluck); } const [dbResult, dbTotal] = await Promise.all([queryPaged, queryTotal]); @@ -157,7 +213,7 @@ export namespace DefaultRoutes { dbResult.map((entry) => { const entryInstance = model.constructor.fromDatabase(entry); Validation.Unlock(this, meta, entryInstance); - return Query.ReadProperties(this, meta, entryInstance, 'list'); + return Query.ReadProperties(this, meta, entryInstance, "list"); }), ); @@ -171,31 +227,39 @@ export namespace DefaultRoutes { }; } - async new(reqCtx: RequestContext, params: Parameters.NewParameters, body: Buffer) { + async new( + _reqCtx: RequestContext, + params: Parameters.NewParameters, + body: Buffer, + ) { const meta = GetDataControllerMeta(this); const data = JSON.parse(body.toString()); if (!params.noMandatory) { - Validation.MandatoryFields(meta, data, 'new'); + Validation.MandatoryFields(meta, data, "new"); } await Validation.ValidateTypes(meta, data); - const dbData = await Query.WriteProperties(this, meta, data, 'new'); + const dbData = await Query.WriteProperties(this, meta, data, "new"); Validation.Lock(this, meta, dbData); const model = Query.GetModel(this, meta); - triggerEvent(dbData, 'insert'); + triggerEvent(dbData, "insert"); const dbResult = await model.table.insert(dbData); return dbResult; } - async edit(reqCtx: RequestContext, params: Parameters.EditParameters, body: Buffer) { + async edit( + _reqCtx: RequestContext, + params: Parameters.EditParameters, + body: Buffer, + ) { const meta = GetDataControllerMeta(this); const data = JSON.parse(body.toString()); if (!params.noMandatory) { - Validation.MandatoryFields(meta, data, 'edit'); + Validation.MandatoryFields(meta, data, "edit"); } await Validation.ValidateTypes(meta, data); @@ -204,14 +268,20 @@ export namespace DefaultRoutes { const queryPrevious = Query.Get(model.table, params.id, params.index); const dbResultPrevious = await queryPrevious; - const dbData = await Query.WriteProperties(this, meta, data, 'edit', dbResultPrevious); + const dbData = await Query.WriteProperties( + this, + meta, + data, + "edit", + dbResultPrevious, + ); Validation.Lock(this, meta, dbData); - triggerEvent(dbData, 'update'); + triggerEvent(dbData, "update"); await model.table.get(params.id).update(dbData); } - async delete(reqCtx: RequestContext, params: Parameters.DeleteParameters) { + async delete(_reqCtx: RequestContext, params: Parameters.DeleteParameters) { const meta = GetDataControllerMeta(this); const model = Query.GetModel(this, meta); @@ -224,18 +294,30 @@ export namespace DefaultRoutes { } } - export const Get = { func: Methods.prototype.get, args: [Context(), Parameters.Get()], method: 'get' }; - export const List = { func: Methods.prototype.list, args: [Context(), Parameters.List()], method: 'get' }; - export const New = { func: Methods.prototype.new, args: [Context(), Parameters.New(), RawBody()], method: 'post' }; + export const Get = { + func: Methods.prototype.get, + args: [Context(), Parameters.Get()], + method: "get", + }; + export const List = { + func: Methods.prototype.list, + args: [Context(), Parameters.List()], + method: "get", + }; + export const New = { + func: Methods.prototype.new, + args: [Context(), Parameters.New(), RawBody()], + method: "post", + }; export const Edit = { func: Methods.prototype.edit, args: [Context(), Parameters.Edit(), RawBody()], - method: 'put', + method: "put", }; export const Delete = { func: Methods.prototype.delete, args: [Context(), Parameters.Delete()], - method: 'delete', + method: "delete", }; export const All = { @@ -251,7 +333,7 @@ export namespace DefaultRoutes { options?: Partial, endpoint?: string, ): DataControllerCallbackWithOptions { - if ('func' in callback) { + if ("func" in callback) { return { endpoint, options, diff --git a/src/test/antelope.test.js b/src/test/antelope.test.js index 0a400d6..c513b27 100644 --- a/src/test/antelope.test.js +++ b/src/test/antelope.test.js @@ -1,25 +1,25 @@ -const { MongoMemoryServer } = require('mongodb-memory-server-core'); +const { MongoMemoryServer } = require("mongodb-memory-server-core"); let mongod; -module.exports.setup = async function () { +module.exports.setup = async () => { mongod = await MongoMemoryServer.create(); return { - cacheFolder: '.antelope/cache', + cacheFolder: ".antelope/cache", modules: { local: { source: { - type: 'local', - path: '.', + type: "local", + path: ".", }, }, mongodb: { source: { - type: 'git', - remote: 'https://github.com/AntelopeJS/mongodb.git', - branch: 'main', - installCommand: ['pnpm i', 'npx tsc'], + type: "git", + remote: "https://github.com/AntelopeJS/mongodb.git", + branch: "main", + installCommand: ["pnpm i", "npx tsc"], }, config: { url: mongod.getUri(), @@ -27,24 +27,24 @@ module.exports.setup = async function () { }, database_decorators: { source: { - type: 'git', - remote: 'https://github.com/AntelopeJS/database-decorators.git', - branch: 'main', - installCommand: ['pnpm i', 'npx tsc'], + type: "git", + remote: "https://github.com/AntelopeJS/database-decorators.git", + branch: "main", + installCommand: ["pnpm i", "npx tsc"], }, }, api: { source: { - type: 'git', - remote: 'https://github.com/AntelopeJS/api.git', - branch: 'main', - installCommand: ['pnpm i', 'npx tsc'], + type: "git", + remote: "https://github.com/AntelopeJS/api.git", + branch: "main", + installCommand: ["pnpm i", "npx tsc"], }, config: { servers: [ { - protocol: 'http', - port: '5010', + protocol: "http", + port: "5010", }, ], }, @@ -53,6 +53,6 @@ module.exports.setup = async function () { }; }; -module.exports.cleanup = async function () { +module.exports.cleanup = async () => { await mongod.stop(); }; diff --git a/src/test/interfaces/data-api/beta/constants.ts b/src/test/interfaces/data-api/beta/constants.ts index e44af64..1ebaa56 100644 --- a/src/test/interfaces/data-api/beta/constants.ts +++ b/src/test/interfaces/data-api/beta/constants.ts @@ -1,3 +1,3 @@ export const API_PORT = 5010; -export const API_HOST = 'localhost'; +export const API_HOST = "localhost"; export const URL_BASE = `http://${API_HOST}:${API_PORT}`;