feat: Migrazione della struttura dei model JS da Coloquent a Spraypaint

Il commit riguarda la rimozione di Coloquent e l'adozione del framework Spraypaint per la struttura dei model. Contiene variazioni nell'implementazione dei metodi associati ai modelli e una modifica delle chiamate per l'ottenimento dei dati. Inoltre, le modifiche aiuteranno a potenziare le prestazioni delle applicazioni.
This commit is contained in:
Maicol Battistini 2023-12-10 17:23:48 +01:00
parent b448da234e
commit 71b03c14b6
No known key found for this signature in database
16 changed files with 112 additions and 348 deletions

View File

@ -11,3 +11,7 @@ rules:
- error
- except-parens
no-plusplus: off
new-cap:
- error
- capIsNewExceptions: ['Model', 'Attr', 'HasMany', 'HasOne', 'BelongsTo', 'Link', 'Stream']
properties: false

View File

@ -18,11 +18,10 @@
"@maicol07/material-web-additions": "^1.4.4",
"@material/mwc-snackbar": "^0.27.0",
"@material/mwc-top-app-bar": "^0.27.0",
"@material/web": "1.0.1",
"@material/web": "^1.0.1",
"@mdi/js": "^7.3.67",
"classnames": "^2.3.2",
"collect.js": "^4.36.1",
"coloquent": "npm:@maicol07/coloquent@3.0.1-beta",
"dayjs": "^1.11.10",
"include-media": "^2.0.0",
"lit": "^3.1.0",
@ -32,6 +31,7 @@
"postcss-scss": "^4.0.9",
"prntr": "^2.0.18",
"readable-http-codes": "^1.1.1",
"spraypaint": "^0.10.24",
"ts-pattern": "^5.0.6",
"tslib": "^2.6.2",
"typescript-cookie": "^1.0.6"

View File

@ -14,7 +14,7 @@ import DataTable, {DataTableAttributes} from '@osm/Components/DataTable/DataTabl
import DataTableColumn, {DataTableColumnAttributes} from '@osm/Components/DataTable/DataTableColumn';
import RecordsTableColumn from '@osm/Components/DataTable/RecordsTableColumn';
import MdIcon from '@osm/Components/MdIcon';
import Model from '@osm/Models/Model';
import Model from '@osm/Models/Record';
import {isVnode} from '@osm/utils/misc';
import collect, {Collection} from 'collect.js';
import {
@ -28,7 +28,7 @@ export interface RecordsTableColumnAttributes extends DataTableColumnAttributes
label?: string;
}
export interface RecordsTableAttributes<M extends Model<any, any>> extends DataTableAttributes {
export interface RecordsTableAttributes<M extends Model> extends DataTableAttributes {
cols: Collection<Children> | Collection<RecordsTableColumnAttributes> | Collection<Children | RecordsTableColumnAttributes>;
records: Map<string, M>;
readonly?: boolean;
@ -42,7 +42,7 @@ export interface RecordsTableAttributes<M extends Model<any, any>> extends DataT
valueModifier?(value: any, attribute: string, record: M): any;
}
export default class RecordsTable<M extends Model<any, any>, A extends RecordsTableAttributes<M> = RecordsTableAttributes<M>> extends DataTable<A> {
export default class RecordsTable<M extends Model, A extends RecordsTableAttributes<M> = RecordsTableAttributes<M>> extends DataTable<A> {
element!: MdDataTable;
selectedRecordsIds: string[] = [];
@ -162,7 +162,7 @@ export default class RecordsTable<M extends Model<any, any>, A extends RecordsTa
cells.put('actions', this.tableRowActions(vnode, record).values<Children>().all());
}
rows.set(record.getId()!, cells);
rows.set(record.id!, cells);
}
return rows;
@ -238,7 +238,7 @@ export default class RecordsTable<M extends Model<any, any>, A extends RecordsTa
protected onDeleteRecordButtonClicked(vnode: Vnode<A>, record: M, event: MouseEvent) {
event.stopPropagation();
vnode.attrs.onDeleteRecordButtonClick?.(record.getId()!, event);
vnode.attrs.onDeleteRecordButtonClick?.(record.id!, event);
}
protected onDeleteSelectedRecordsButtonClicked(vnode: Vnode<A>, event: MouseEvent) {
@ -247,9 +247,10 @@ export default class RecordsTable<M extends Model<any, any>, A extends RecordsTa
protected getModelValue(record: M, attribute: string, vnode: Vnode<A>): unknown {
// Check if is a relation
let value: unknown = record.getAttribute(attribute);
// @ts-expect-error
let value: unknown = record[attribute];
if (attribute === 'id') {
value = record.getId();
value = record.id;
}
return vnode.attrs.valueModifier?.(value, attribute, record) ?? value;

View File

@ -1,7 +1,7 @@
import {mdiFloppy} from '@mdi/js';
import RecordDialog, {RecordDialogAttributes} from '@osm/Components/Dialogs/RecordDialog';
import MdIcon from '@osm/Components/MdIcon';
import Model from '@osm/Models/Model';
import Model from '@osm/Models/Record';
import {
VnodeCollection,
VnodeCollectionItem
@ -13,7 +13,6 @@ import {
showSnackbar
} from '@osm/utils/misc';
import collect, {Collection} from 'collect.js';
import {SaveResponse} from 'coloquent';
import {
Children,
Vnode,
@ -23,7 +22,7 @@ import Stream from 'mithril/stream';
import {Form} from 'mithril-utilities';
import {Class} from 'type-fest';
export default abstract class AddEditRecordDialog<M extends Model<any, any>> extends RecordDialog<M> {
export default abstract class AddEditRecordDialog<M extends Model> extends RecordDialog<M> {
// eslint-disable-next-line unicorn/no-null
protected formElement: HTMLFormElement | null = null;
protected abstract formState: Map<string, Stream<any>>;
@ -47,12 +46,13 @@ export default abstract class AddEditRecordDialog<M extends Model<any, any>> ext
super.oncreate(vnode);
this.formElement = this.element.querySelector('form');
this.element.querySelector(`#saveBtn${this.formId}`)?.setAttribute('form', this.formId!);
this.element.querySelector(`#saveBtn${this.formId!}`)?.setAttribute('form', this.formId!);
}
fillForm() {
for (const [key, value] of this.formState) {
value(this.record.getAttribute(key) ?? value());
// @ts-ignore
value(this.record[key] ?? value());
}
}
@ -126,27 +126,25 @@ export default abstract class AddEditRecordDialog<M extends Model<any, any>> ext
}
async save(): Promise<boolean> {
this.record.setAttributes(this.modelAttributesFromFormState);
this.record.assignAttributes(this.modelAttributesFromFormState);
try {
const response = await this.record.save();
this.afterSave(response);
return response.getModelId() !== undefined;
const result = await this.record.save();
this.afterSave(result);
return result;
} catch (error) {
this.onSaveError(error as JSONAPI.RequestError);
return false;
}
}
afterSave(response: SaveResponse<M>): void {
const responseModel = response.getModel() as M;
if (responseModel !== undefined) {
this.record = responseModel;
afterSave(result: boolean): void {
if (result) {
void showSnackbar(__('Record salvato con successo'));
}
}
onSaveError(error: JSONAPI.RequestError): void {
const message = error.response.data.message;
const {message} = error.response.data;
void showSnackbar(message, false);
}

View File

@ -1,6 +1,6 @@
import {mdiDelete} from '@mdi/js';
import MdIcon from '@osm/Components/MdIcon';
import Model from '@osm/Models/Model';
import Model from '@osm/Models/Record';
import {VnodeCollection} from '@osm/typings/jsx';
import {showSnackbar} from '@osm/utils/misc';
import collect from 'collect.js';
@ -12,11 +12,11 @@ import {RequestError} from 'mithril-utilities';
import RecordDialog, {RecordDialogAttributes} from './RecordDialog';
export interface DeleteRecordDialogAttributes<M extends Model<any, any>> extends RecordDialogAttributes<M> {
export interface DeleteRecordDialogAttributes<M extends Model> extends RecordDialogAttributes<M> {
records: M | M[];
}
export default class DeleteRecordDialog<M extends Model<any, any>, A extends DeleteRecordDialogAttributes<M> = DeleteRecordDialogAttributes<M>> extends RecordDialog<M, A> {
export default class DeleteRecordDialog<M extends Model, A extends DeleteRecordDialogAttributes<M> = DeleteRecordDialogAttributes<M>> extends RecordDialog<M, A> {
records!: M[];
oninit(vnode: Vnode<A, this>) {
@ -32,7 +32,7 @@ export default class DeleteRecordDialog<M extends Model<any, any>, A extends Del
return (
<>
<p>{text}</p>
<ul>{this.records.map((record) => <li key={record.getId()}>{this.recordSummary(record, vnode)}</li>)}</ul>
<ul>{this.records.map((record) => <li key={record.id}>{this.recordSummary(record, vnode)}</li>)}</ul>
</>
);
}
@ -61,7 +61,7 @@ export default class DeleteRecordDialog<M extends Model<any, any>, A extends Del
}
recordSummary(record: M, vnode: Vnode<A, this>): Children {
return __('ID: :recordId', {recordId: record.getId()!});
return __('ID: :recordId', {recordId: record.id!});
}
async onConfirmButtonClicked() {
@ -74,12 +74,12 @@ export default class DeleteRecordDialog<M extends Model<any, any>, A extends Del
async deleteRecord() {
try {
const promises = this.records.map((record) => record.delete());
const promises = this.records.map((record) => record.destroy());
await Promise.all(promises);
// TODO: Better way for pluralization in i18n
void showSnackbar(this.records.length > 1 ? __('Record eliminati!') : __('Record eliminato!'));
this.close('deleted');
void this.close('deleted');
} catch (error) {
void showSnackbar(__('Errore durante l\'eliminazione del record! :error', {error: (error as RequestError<{message: string}>).response.message}), false);
}

View File

@ -2,7 +2,7 @@ import '@maicol07/material-web-additions/layout-grid/layout-grid.js';
import '@material/web/button/text-button.js';
import Dialog, {DialogAttributes} from '@osm/Components/Dialogs/Dialog';
import Model from '@osm/Models/Model';
import Model from '@osm/Models/Record';
import {Vnode} from 'mithril';
export interface RecordDialogAttributes<M extends Model<any, any>> extends DialogAttributes {

View File

@ -3,27 +3,27 @@ import '@material/web/button/outlined-button.js';
import {mdiChevronLeft} from '@mdi/js';
import MdIcon from '@osm/Components/MdIcon';
import Page, {PageAttributes} from '@osm/Components/Page';
import Model from '@osm/Models/Model';
import Model from '@osm/Models/Record';
import {showSnackbar} from '@osm/utils/misc';
import {Builder} from 'coloquent';
import {
Children,
Vnode
} from 'mithril';
import {Scope} from 'spraypaint';
import {Class} from 'type-fest';
export interface RecordPageAttributes<M extends Model<any, any>> extends PageAttributes {
export interface RecordPageAttributes<M extends Model> extends PageAttributes {
record: M;
}
export default abstract class RecordPage<M extends Model<any, any>, A extends RecordPageAttributes<M> = RecordPageAttributes<M>> extends Page<A> {
abstract recordType: Class<M> & typeof Model<any, any>;
export default abstract class RecordPage<M extends Model, A extends RecordPageAttributes<M> = RecordPageAttributes<M>> extends Page<A> {
abstract recordType: Class<M> & typeof Model;
record?: M;
async oninit(vnode: Vnode<A, this>) {
super.oninit(vnode);
const {id: recordId} = route().params as {id: number | string};
if (recordId !== this.record?.getId()) {
if (recordId !== this.record?.id) {
await this.loadRecord(recordId);
}
}
@ -32,24 +32,23 @@ export default abstract class RecordPage<M extends Model<any, any>, A extends Re
if (recordId && recordId !== 'new' && !this.record) {
try {
const response = await this.modelQuery().find(recordId);
this.record = response.getData() || undefined;
} catch (e) {
console.error(e);
this.record = response.data || undefined;
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
void showSnackbar(__('Errore durante il caricamento del record'));
// Do nothing
}
}
if (!this.record) {
// @ts-expect-error — This won't be abstract when implemented
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.record = new this.recordType();
}
m.redraw();
}
modelQuery(): Builder<M> {
return this.recordType.query();
modelQuery(): Scope<M> {
return this.recordType as unknown as Scope<M>;
}
contents(vnode: Vnode<A>): Children {

View File

@ -21,9 +21,8 @@ import DeleteRecordDialog, {DeleteRecordDialogAttributes} from '@osm/Components/
import RecordDialog, {RecordDialogAttributes} from '@osm/Components/Dialogs/RecordDialog';
import MdIcon from '@osm/Components/MdIcon';
import Page, {PageAttributes} from '@osm/Components/Page';
import Model from '@osm/Models/Model';
import Record from '@osm/Models/Record';
import collect, {type Collection} from 'collect.js';
import {SortDirection} from 'coloquent';
import dayjs from 'dayjs';
import type {
Children,
@ -31,38 +30,25 @@ import type {
VnodeDOM
} from 'mithril';
import Stream from 'mithril/stream';
import {Scope} from 'spraypaint';
import {SortDir} from 'spraypaint/lib-esm/scope';
import {match} from 'ts-pattern';
import {Match} from 'ts-pattern/dist/types/Match';
import type {Class} from 'type-fest';
export interface Meta {
current_page: number;
from: number;
last_page: number;
path: string;
per_page: number;
to: number;
total: number;
}
export interface JSONAPIResponse {
meta: Meta;
}
type RecordDialogVnode<M extends Model<any, any>, D extends RecordDialog<M>> = Vnode<RecordDialogAttributes<M>, D>;
type DeleteRecordDialogVnode<M extends Model<any, any>, D extends DeleteRecordDialog<M>> = Vnode<DeleteRecordDialogAttributes<M>, D>;
type RecordDialogVnode<M extends Record, D extends RecordDialog<M>> = Vnode<RecordDialogAttributes<M>, D>;
type DeleteRecordDialogVnode<M extends Record, D extends DeleteRecordDialog<M>> = Vnode<DeleteRecordDialogAttributes<M>, D>;
// noinspection JSUnusedLocalSymbols
/**
* @abstract
*/
export default abstract class RecordsPage<
M extends Model<any, any>,
M extends Record,
D extends AddEditRecordDialog<M> = AddEditRecordDialog<M>,
DRD extends DeleteRecordDialog<M> = DeleteRecordDialog<M>
> extends Page {
abstract modelType: Class<M> & typeof Model<any, any>;
abstract modelType: Class<M> & typeof Record;
recordDialogType?: Class<D>;
deleteRecordDialogType?: Class<DRD>;
@ -86,15 +72,13 @@ export default abstract class RecordsPage<
protected lastRowOfPage = this.totalRecords;
protected filters: Map<string, string> = new Map();
protected sort: Map<string, SortDirection> = new Map([['id', SortDirection.ASC]]);
protected sort: Map<string, SortDir> = new Map();
protected relatedFilters: Map<string, string> = new Map();
private listenedFilterColumns: string[] = [];
private listenedSortedColumns: string[] = [];
oninit(vnode: Vnode<PageAttributes, this>) {
super.oninit(vnode);
// @ts-ignore
this.modelType.pageSize = this.currentPageSize;
// Redraw on a first load to call onbeforeupdate
m.redraw();
}
@ -111,24 +95,21 @@ export default abstract class RecordsPage<
async loadRecords() {
this.isTableLoading = true;
let query = this.modelQuery();
const query = this.modelQuery();
// Fix Restify when filtering relations
query = query.option('related', query.getQuery().getInclude().join(','));
const response = await query.get(this.currentPage);
const rawResponse = response.getHttpClientResponse().getData() as JSONAPIResponse;
this.lastPage = rawResponse.meta.last_page;
this.firstRowOfPage = rawResponse.meta.from;
this.lastRowOfPage = rawResponse.meta.to;
this.currentPageSize = rawResponse.meta.per_page;
this.totalRecords = rawResponse.meta.total;
const data = response.getData();
const response = await query.page(this.currentPage).all();
const rawResponse = response.raw;
this.lastPage = rawResponse.meta?.last_page as number;
this.firstRowOfPage = rawResponse.meta?.from as number;
this.lastRowOfPage = rawResponse.meta?.to as number;
this.currentPageSize = rawResponse.meta?.per_page as number;
this.totalRecords = rawResponse.meta?.total as number;
const {data} = response;
this.records.clear();
if (data.length > 0) {
for (const record of data) {
this.records.set(record.getId()!, record);
this.records.set(record.id, record);
}
}
@ -144,31 +125,29 @@ export default abstract class RecordsPage<
*/
private static convertToSnakeCase(string_: string, trim = false, removeSpecials = false, underscoredNumbers = false) {
return string_.replace(removeSpecials ? /[^\w ]/g : '', '')
.replace(/([ _]+)/g, '_')
.replace(/[ _]+/g, '_')
.replace(trim ? /(^_|_$)/gm : '', '')
.replace(underscoredNumbers ? /([^\dA-Z_])([^_a-z])/g : /([^\dA-Z_])([^\d_a-z])/g, (m, preUpper, upper) => `${preUpper}_${upper}`)
.replace(underscoredNumbers ? /([^\d_]\d|\d[^\d_])/g : '', (m, index) => (index ? index.split('').join('_') : ''))
.replace(/([A-Z])([A-Z])([^\dA-Z_])/g, (m, previousUpper, upper, lower) => `${previousUpper}_${upper}${lower}`)
.replaceAll('_.', '.') // remove redundant underscores
.replace(underscoredNumbers ? /([^\dA-Z_])([^_a-z])/g : /([^\dA-Z_])([^\d_a-z])/g, (m, preUpper: string, upper: string) => `${preUpper}_${upper}`)
.replace(underscoredNumbers ? /([^\d_]\d|\d[^\d_])/g : '', (m, index: string) => (index ? [...index].join('_') : ''))
.replace(/([A-Z])([A-Z])([^\dA-Z_])/g, (m, previousUpper: string, upper: string, lower: string) => `${previousUpper}_${upper}${lower}`)
.replaceAll('_.', '.') // Remove redundant underscores
.toLowerCase();
}
modelQuery() {
// @ts-ignore
let query = this.modelType.query<M>();
let query: Scope<M> = this.modelType.per(this.currentPageSize);
for (const [attribute, value] of this.filters) {
// Query = query.where(attribute, value); TODO: Revert when Restify uses JSONAPI syntax
query = query.option(RecordsPage.convertToSnakeCase(attribute), value);
query = query.where({[RecordsPage.convertToSnakeCase(attribute)]: value});
}
for (const [relation, value] of this.relatedFilters) {
query = query.option('related', relation)
.option('search', value);
query = query.where({related: relation, search: value}); // TODO: Check
// .where('search', value);
}
for (const [attribute, value] of this.sort) {
query = query.orderBy(RecordsPage.convertToSnakeCase(attribute), value);
query = query.order({[RecordsPage.convertToSnakeCase(attribute)]: value});
}
return query;
@ -244,7 +223,7 @@ export default abstract class RecordsPage<
updateRecord(model: M) {
if (this.recordPageRouteName) {
router.visit(route(this.recordPageRouteName, {id: model.getId()!}));
router.visit(route(this.recordPageRouteName, {id: model.id}));
return;
}
@ -258,8 +237,8 @@ export default abstract class RecordsPage<
for (const [key, state] of this.recordDialogsStates) {
// noinspection LocalVariableNamingConventionJS
const RD = this.recordDialogType!;
const record = key instanceof Model ? key : this.records.get(key);
const vnodeKey = record?.getId() ?? (key as string);
const record = key instanceof Record ? key : this.records.get(key);
const vnodeKey = record?.id ?? (key as string);
collection.put(vnodeKey, <RD key={vnodeKey} record={record} open={state}/>);
}
@ -303,14 +282,16 @@ export default abstract class RecordsPage<
onTablePageChange(event: CustomEvent<PaginateDetail>) {
const {pageSize, action} = event.detail;
this.currentPageSize = pageSize;
const {currentPage} = this;
const {currentPage, lastPage} = this;
match(action)
.with('first', () => (this.currentPage = 1))
.with('previous', () => (this.currentPage--))
.with('next', () => (this.currentPage++))
.with('last', () => (this.currentPage = this.lastPage))
.with('last', () => (this.currentPage = lastPage))
.with('current', () => {})
.run();
// We need to check if the page has changed
// eslint-disable-next-line unicorn/consistent-destructuring
if (currentPage !== this.currentPage) {
this.refreshRecords = true;
m.redraw();
@ -356,13 +337,13 @@ export default abstract class RecordsPage<
const {column, isDescending} = (event as CustomEvent<SortButtonClickedEventDetail>).detail;
const modelAttribute = column.dataset.sortAttribute ?? column.dataset.modelAttribute!;
this.sort.clear();
this.sort.set(modelAttribute, isDescending ? SortDirection.DESC : SortDirection.ASC);
this.sort.set(modelAttribute, isDescending ? 'desc' : 'asc');
this.refreshRecords = true;
m.redraw();
}
openDeleteRecordsDialog(records: M | M[]) {
const key = records instanceof Model ? records.getId()! : records.map((r) => r.getId()).join(',');
const key = records instanceof Record ? records.id! : records.map((r) => r.id).join(',');
let state = this.deleteRecordsDialogStates.get(key);
if (!state) {
@ -386,7 +367,7 @@ export default abstract class RecordsPage<
}
protected getRecordDialogState(record?: M, slug?: string) {
const key: string = slug ?? record?.getId() ?? '';
const key: string = slug ?? record?.id ?? '';
if (!this.recordDialogsStates.has(key)) {
const state = Stream<boolean>(false);

View File

@ -1,74 +0,0 @@
import type {
HttpClient,
HttpClientPromise
} from 'coloquent';
import {Request} from 'mithril-utilities';
import RequestHttpClientPromise from './RequestHttpClientPromise';
/**
* @class RequestHttpClient
*
* NOTE: This class is not meant to be used directly, but only for Models.
* You should use the {@link Request} class instead.
*/
export default class RequestHttpClient implements HttpClient {
request: Request;
constructor(requestInstance?: Request) {
this.request = requestInstance ?? new Request({
background: true,
headers: {
Accept: 'application/vnd.api+json',
'Content-Type': 'application/vnd.api+json'
}
});
}
delete(url: string): HttpClientPromise {
this.request.options.method = 'delete';
this.request.options.url = url;
return new RequestHttpClientPromise(this.request.send());
}
get(url: string): HttpClientPromise {
this.request.options.method = 'get';
this.request.options.url = url;
return new RequestHttpClientPromise(this.request.send());
}
getImplementingClient() {
return this.request;
}
head(url: string): HttpClientPromise {
this.request.options.method = 'head';
this.request.options.url = url;
return new RequestHttpClientPromise(this.request.send());
}
patch(url: string, data?: Record<string, any>): HttpClientPromise {
this.request.options.method = 'patch';
this.request.options.url = url;
this.request.options.body = data;
return new RequestHttpClientPromise(this.request.send());
}
post(url: string, data?: Record<string, any>): HttpClientPromise {
this.request.options.method = 'post';
this.request.options.url = url;
this.request.options.body = data;
return new RequestHttpClientPromise(this.request.send());
}
put(url: string, data?: Record<string, any>): HttpClientPromise {
this.request.options.method = 'put';
this.request.options.url = url;
this.request.options.body = data;
return new RequestHttpClientPromise(this.request.send());
}
setWithCredentials(withCredientials: boolean): void {
this.request.options.withCredentials = withCredientials;
}
}

View File

@ -1,29 +0,0 @@
import RequestHttpClientResponse from '@osm/Models/Http/RequestHttpClientResponse';
import type {
HttpClientPromise,
HttpClientResponse
} from 'coloquent';
import type {Thenable} from 'coloquent/dist/httpclient/Types';
export default class RequestHttpClientPromise implements HttpClientPromise {
constructor(private response: Promise<any>) {}
catch<U>(onRejected?: (error: any) => (Thenable<U> | U)): Promise<U> {
return this.response.catch(onRejected) as Promise<U>;
}
// eslint-disable-next-line unicorn/no-thenable
then<U>(
onFulfilled?: (value: HttpClientResponse) => (Thenable<U> | U),
onRejected?: (error: any) => void | (Thenable<U> | U)
): Promise<U> {
const wrappedOnFulfilled = onFulfilled === undefined
? undefined
: ((responsePromise: any) => onFulfilled(new RequestHttpClientResponse(responsePromise)));
return this.response.then<U>(
wrappedOnFulfilled,
// @ts-ignore
onRejected
);
}
}

View File

@ -1,13 +0,0 @@
import type {HttpClientResponse} from 'coloquent';
export default class RequestHttpClientResponse implements HttpClientResponse {
constructor(private response: any) {}
getData(): any {
return this.response;
}
getUnderlying(): any {
return this.response;
}
}

View File

@ -1,113 +0,0 @@
import RequestHttpClient from '@osm/Models/Http/RequestHttpClient';
import {
Model as BaseModel,
PaginationStrategy,
PluralResponse
} from 'coloquent';
import dayjs from 'dayjs';
import type {ValueOf} from 'type-fest';
export interface ModelAttributes {
id: number;
createdAt: Date;
updatedAt: Date;
[key: string]: unknown;
}
export interface ModelRelations {
[key: string]: unknown;
}
/**
* The base model for all models.
*/
export default abstract class Model<A extends ModelAttributes, R extends ModelRelations> extends BaseModel {
protected static paginationStrategy = PaginationStrategy.PageBased;
protected static jsonApiBaseUrl = '/api/restify';
getHttpClient() { return new RequestHttpClient(); }
static dates: Record<string, string> = {
createdAt: 'YYYY-MM-DDTHH:mm:ss.ssssssZ',
updatedAt: 'YYYY-MM-DDTHH:mm:ss.ssssssZ'
};
protected static get jsonApiType() {
return `${this.name.toLowerCase()}s`;
}
/**
* Returns all the instances of the model. (Alias of {@link Model.get}).
*/
static all<M extends typeof Model<any, any> & {
new (): InstanceType<M>;
// @ts-ignore
}>(this: M): Promise<PluralResponse<InstanceType<M>>> {
// @ts-expect-error
return this.get();
}
/**
* Set multiple attributes on the model.
*/
setAttributes(attributes: Partial<A> | Map<keyof A, ValueOf<A>>) {
// Record to map
if (!(attributes instanceof Map)) {
// eslint-disable-next-line no-param-reassign
attributes = new Map(Object.entries(attributes) as [keyof A, ValueOf<A>][]);
}
for (const [attribute, value] of attributes) {
this.setAttribute(attribute, value);
}
}
getAttribute<AN extends keyof A = keyof A>(attributeName: AN) {
return super.getAttribute(attributeName as string) as ValueOf<A, AN>;
}
getAttributes() {
return super.getAttributes() as ModelAttributes;
}
protected getAttributeAsDate(attributeName: string) {
// @ts-ignore
let attribute: string | Date = (this.attributes as Map<string, string | null>).get(attributeName);
if (attribute && dayjs(attribute).isValid()) {
attribute = super.getAttributeAsDate(attributeName) as Date;
}
return attribute;
}
setAttribute<AN extends keyof A = keyof A>(attributeName: AN, value: ValueOf<A, AN>) {
const date = dayjs(value as string | Date | undefined);
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if (this.isDateAttribute(attributeName) && date.isValid()) {
const format = this.constructor.dates[attributeName as string];
// eslint-disable-next-line no-param-reassign
value = (format === 'YYYY-MM-DDTHH:mm:ss.ssssssZ' ? date.toISOString() : date.format(format)) as ValueOf<A, AN>;
}
// @ts-expect-error — This is needed to parse the dates correctly.
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
this.attributes.set(attributeName as string, value);
}
getRelation<RN extends keyof R = keyof R>(relationName: RN) {
return super.getRelation(relationName as string) as ValueOf<R, RN>;
}
/**
* Get model ID.
*/
getId() {
return this.getApiId();
}
/**
* Check if the model is new (not already saved).
*/
isNew() {
return this.getId() === undefined;
}
}

View File

@ -0,0 +1,15 @@
import {Attr, Model, SpraypaintBase} from 'spraypaint';
@Model()
export default class Record extends SpraypaintBase {
static baseUrl = '';
static apiNamespace = '/api/restify';
static clientApplication = 'OpenSTAManager';
@Attr({persist: false}) createdAt!: string;
@Attr({persist: false}) updatedAt!: string;
isNew(): boolean {
return this.id === undefined;
}
}

View File

@ -1,15 +1,10 @@
import Model, {
ModelAttributes,
ModelRelations
} from '@osm/Models/Model';
import Record from '@osm/Models/Record';
import {Attr, Model} from 'spraypaint';
export interface UserAttributes extends ModelAttributes {
username: string;
email: string;
@Model()
export default class User extends Record {
static jsonapiType = 'users';
@Attr() username!: string;
@Attr() email!: string;
}
export interface UserRelations extends ModelRelations {
// Notifications: DatabaseNotifications
}
export default class User extends Model<UserAttributes, UserRelations> {}

View File

@ -12,9 +12,9 @@ export default class UserRecord extends RecordPage<User> {
return (
<>
{this.backButton(vnode)}
<h1>{this.record?.getAttribute('username')}</h1>
<h1>{this.record?.username}</h1>
<code>
{JSON.stringify(this.record?.getAttributes())}
{JSON.stringify(this.record?.attributes)}
</code>
</>
);

View File

@ -35,9 +35,9 @@ export default class UsersRecordDialog extends AddEditRecordDialog<User> {
}
async save() {
if (this.record.isNew()) {
this.record.setAttribute('password', 'default');
}
// if (this.record.isNew()) {
// this.record.password = 'default';
// }
return super.save();
}
}