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/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/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/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/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/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/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}`; 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[],