@stonyx/orm 0.2.1-beta.9 → 0.2.1-beta.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -6
- package/config/environment.js +37 -1
- package/dist/aggregates.d.ts +21 -0
- package/dist/aggregates.js +93 -0
- package/dist/attr.d.ts +2 -0
- package/dist/attr.js +22 -0
- package/dist/belongs-to.d.ts +11 -0
- package/dist/belongs-to.js +59 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +148 -0
- package/dist/commands.d.ts +7 -0
- package/dist/commands.js +146 -0
- package/dist/db.d.ts +21 -0
- package/dist/db.js +180 -0
- package/dist/exports/db.d.ts +7 -0
- package/{src → dist}/exports/db.js +2 -4
- package/dist/has-many.d.ts +11 -0
- package/dist/has-many.js +58 -0
- package/dist/hooks.d.ts +62 -0
- package/dist/hooks.js +110 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +34 -0
- package/dist/main.d.ts +46 -0
- package/dist/main.js +181 -0
- package/dist/manage-record.d.ts +13 -0
- package/dist/manage-record.js +123 -0
- package/dist/meta-request.d.ts +6 -0
- package/dist/meta-request.js +52 -0
- package/dist/migrate.d.ts +2 -0
- package/dist/migrate.js +57 -0
- package/dist/model-property.d.ts +9 -0
- package/dist/model-property.js +29 -0
- package/dist/model.d.ts +15 -0
- package/dist/model.js +18 -0
- package/dist/mysql/connection.d.ts +14 -0
- package/dist/mysql/connection.js +24 -0
- package/dist/mysql/migration-generator.d.ts +45 -0
- package/dist/mysql/migration-generator.js +254 -0
- package/dist/mysql/migration-runner.d.ts +12 -0
- package/dist/mysql/migration-runner.js +88 -0
- package/dist/mysql/mysql-db.d.ts +100 -0
- package/dist/mysql/mysql-db.js +425 -0
- package/dist/mysql/query-builder.d.ts +10 -0
- package/dist/mysql/query-builder.js +44 -0
- package/dist/mysql/schema-introspector.d.ts +19 -0
- package/dist/mysql/schema-introspector.js +291 -0
- package/dist/mysql/type-map.d.ts +21 -0
- package/dist/mysql/type-map.js +36 -0
- package/dist/orm-request.d.ts +38 -0
- package/dist/orm-request.js +474 -0
- package/dist/plural-registry.d.ts +4 -0
- package/dist/plural-registry.js +9 -0
- package/dist/postgres/connection.d.ts +15 -0
- package/dist/postgres/connection.js +32 -0
- package/dist/postgres/migration-generator.d.ts +45 -0
- package/dist/postgres/migration-generator.js +261 -0
- package/dist/postgres/migration-runner.d.ts +10 -0
- package/dist/postgres/migration-runner.js +87 -0
- package/dist/postgres/postgres-db.d.ts +119 -0
- package/dist/postgres/postgres-db.js +477 -0
- package/dist/postgres/query-builder.d.ts +27 -0
- package/dist/postgres/query-builder.js +98 -0
- package/dist/postgres/schema-introspector.d.ts +29 -0
- package/dist/postgres/schema-introspector.js +314 -0
- package/dist/postgres/type-map.d.ts +23 -0
- package/dist/postgres/type-map.js +56 -0
- package/dist/record.d.ts +75 -0
- package/dist/record.js +129 -0
- package/dist/relationships.d.ts +10 -0
- package/dist/relationships.js +41 -0
- package/dist/serializer.d.ts +17 -0
- package/dist/serializer.js +136 -0
- package/dist/setup-rest-server.d.ts +1 -0
- package/dist/setup-rest-server.js +52 -0
- package/dist/standalone-db.d.ts +58 -0
- package/dist/standalone-db.js +142 -0
- package/dist/store.d.ts +62 -0
- package/dist/store.js +286 -0
- package/dist/timescale/query-builder.d.ts +43 -0
- package/dist/timescale/query-builder.js +115 -0
- package/dist/timescale/timescale-db.d.ts +45 -0
- package/dist/timescale/timescale-db.js +84 -0
- package/dist/transforms.d.ts +2 -0
- package/dist/transforms.js +17 -0
- package/dist/types/orm-types.d.ts +142 -0
- package/dist/types/orm-types.js +1 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +17 -0
- package/dist/view-resolver.d.ts +8 -0
- package/dist/view-resolver.js +171 -0
- package/dist/view.d.ts +11 -0
- package/dist/view.js +18 -0
- package/package.json +57 -15
- package/src/aggregates.ts +109 -0
- package/src/{attr.js → attr.ts} +2 -2
- package/src/belongs-to.ts +90 -0
- package/src/cli.ts +183 -0
- package/src/{commands.js → commands.ts} +179 -170
- package/src/{db.js → db.ts} +55 -29
- package/src/exports/db.ts +7 -0
- package/src/has-many.ts +92 -0
- package/src/{hooks.js → hooks.ts} +41 -27
- package/src/{index.js → index.ts} +11 -2
- package/src/main.ts +229 -0
- package/src/manage-record.ts +161 -0
- package/src/{meta-request.js → meta-request.ts} +17 -14
- package/src/{migrate.js → migrate.ts} +9 -9
- package/src/model-property.ts +35 -0
- package/src/model.ts +21 -0
- package/src/mysql/{connection.js → connection.ts} +43 -28
- package/src/mysql/migration-generator.ts +337 -0
- package/src/mysql/{migration-runner.js → migration-runner.ts} +121 -110
- package/src/mysql/mysql-db.ts +543 -0
- package/src/mysql/{query-builder.js → query-builder.ts} +69 -64
- package/src/mysql/schema-introspector.ts +358 -0
- package/src/mysql/{type-map.js → type-map.ts} +42 -37
- package/src/{orm-request.js → orm-request.ts} +186 -108
- package/src/plural-registry.ts +12 -0
- package/src/postgres/connection.ts +48 -0
- package/src/postgres/migration-generator.ts +348 -0
- package/src/postgres/migration-runner.ts +115 -0
- package/src/postgres/postgres-db.ts +616 -0
- package/src/postgres/query-builder.ts +148 -0
- package/src/postgres/schema-introspector.ts +386 -0
- package/src/postgres/type-map.ts +61 -0
- package/src/record.ts +186 -0
- package/src/relationships.ts +54 -0
- package/src/serializer.ts +161 -0
- package/src/{setup-rest-server.js → setup-rest-server.ts} +18 -16
- package/src/standalone-db.ts +185 -0
- package/src/store.ts +373 -0
- package/src/timescale/query-builder.ts +174 -0
- package/src/timescale/timescale-db.ts +119 -0
- package/src/transforms.ts +20 -0
- package/src/types/mysql2.d.ts +49 -0
- package/src/types/orm-types.ts +146 -0
- package/src/types/pg.d.ts +32 -0
- package/src/types/stonyx-cron.d.ts +5 -0
- package/src/types/stonyx-events.d.ts +4 -0
- package/src/types/stonyx-rest-server.d.ts +16 -0
- package/src/types/stonyx-utils.d.ts +33 -0
- package/src/types/stonyx.d.ts +21 -0
- package/src/utils.ts +22 -0
- package/src/view-resolver.ts +211 -0
- package/src/view.ts +22 -0
- package/.claude/code-style-rules.md +0 -44
- package/.claude/hooks.md +0 -250
- package/.claude/index.md +0 -279
- package/.claude/usage-patterns.md +0 -217
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -51
- package/improvements.md +0 -139
- package/project-structure.md +0 -343
- package/src/belongs-to.js +0 -63
- package/src/has-many.js +0 -61
- package/src/main.js +0 -148
- package/src/manage-record.js +0 -118
- package/src/model-property.js +0 -29
- package/src/model.js +0 -9
- package/src/mysql/migration-generator.js +0 -188
- package/src/mysql/mysql-db.js +0 -320
- package/src/mysql/schema-introspector.js +0 -158
- package/src/record.js +0 -127
- package/src/relationships.js +0 -43
- package/src/serializer.js +0 -138
- package/src/store.js +0 -211
- package/src/transforms.js +0 -20
- package/src/utils.js +0 -12
- package/test-events-setup.js +0 -41
- package/test-hooks-manual.js +0 -54
- package/test-hooks-with-logging.js +0 -52
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
declare module 'mysql2/promise' {
|
|
2
|
+
interface PoolOptions {
|
|
3
|
+
host: string;
|
|
4
|
+
user: string;
|
|
5
|
+
password: string;
|
|
6
|
+
database: string;
|
|
7
|
+
port?: number;
|
|
8
|
+
waitForConnections?: boolean;
|
|
9
|
+
connectionLimit?: number;
|
|
10
|
+
queueLimit?: number;
|
|
11
|
+
enableKeepAlive?: boolean;
|
|
12
|
+
keepAliveInitialDelay?: number;
|
|
13
|
+
[key: string]: string | number | boolean | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ExecuteResult {
|
|
17
|
+
insertId: number;
|
|
18
|
+
affectedRows: number;
|
|
19
|
+
changedRows: number;
|
|
20
|
+
fieldCount: number;
|
|
21
|
+
info: string;
|
|
22
|
+
serverStatus: number;
|
|
23
|
+
warningStatus: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface FieldPacket {
|
|
27
|
+
name: string;
|
|
28
|
+
type: number;
|
|
29
|
+
length: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type RowDataPacket = Record<string, string | number | boolean | null>;
|
|
33
|
+
|
|
34
|
+
interface Pool {
|
|
35
|
+
execute(sql: string, params?: unknown[]): Promise<[RowDataPacket[] | ExecuteResult, FieldPacket[]]>;
|
|
36
|
+
query(sql: string, params?: unknown[]): Promise<[RowDataPacket[] | ExecuteResult, FieldPacket[]]>;
|
|
37
|
+
end(): Promise<void>;
|
|
38
|
+
getConnection(): Promise<PoolConnection>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface PoolConnection extends Pool {
|
|
42
|
+
release(): void;
|
|
43
|
+
beginTransaction(): Promise<void>;
|
|
44
|
+
commit(): Promise<void>;
|
|
45
|
+
rollback(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createPool(options: PoolOptions): Pool;
|
|
49
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { AggregateProperty } from '../aggregates.js';
|
|
2
|
+
|
|
3
|
+
export interface OrmDbConfig {
|
|
4
|
+
file: string;
|
|
5
|
+
schema: string;
|
|
6
|
+
mode: string;
|
|
7
|
+
directory: string;
|
|
8
|
+
autosave: string;
|
|
9
|
+
saveInterval: string | number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface OrmMysqlConfig {
|
|
13
|
+
host: string;
|
|
14
|
+
port?: number;
|
|
15
|
+
user: string;
|
|
16
|
+
password: string;
|
|
17
|
+
database: string;
|
|
18
|
+
connectionLimit?: number;
|
|
19
|
+
migrationsDir?: string;
|
|
20
|
+
migrationsTable?: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OrmPostgresConfig {
|
|
25
|
+
host: string;
|
|
26
|
+
port: number;
|
|
27
|
+
user: string;
|
|
28
|
+
password: string;
|
|
29
|
+
database: string;
|
|
30
|
+
connectionLimit?: number;
|
|
31
|
+
migrationsDir?: string;
|
|
32
|
+
migrationsTable?: string;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OrmPaths {
|
|
37
|
+
model: string;
|
|
38
|
+
serializer: string;
|
|
39
|
+
transform: string;
|
|
40
|
+
view?: string;
|
|
41
|
+
access?: string;
|
|
42
|
+
[key: string]: string | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface OrmRestServerConfig {
|
|
46
|
+
enabled: string;
|
|
47
|
+
route: string;
|
|
48
|
+
metaRoute: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface OrmSection {
|
|
52
|
+
db: OrmDbConfig;
|
|
53
|
+
paths: OrmPaths;
|
|
54
|
+
restServer: OrmRestServerConfig;
|
|
55
|
+
mysql?: OrmMysqlConfig;
|
|
56
|
+
postgres?: OrmPostgresConfig;
|
|
57
|
+
timescale?: OrmPostgresConfig;
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface OrmConfig {
|
|
62
|
+
rootPath: string;
|
|
63
|
+
orm: OrmSection;
|
|
64
|
+
[key: string]: unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface SourceRecord {
|
|
68
|
+
__model: { __name: string; [key: string]: unknown };
|
|
69
|
+
__data?: Record<string, unknown>;
|
|
70
|
+
__relationships?: Record<string, unknown>;
|
|
71
|
+
id: string | number;
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface OrmRecord {
|
|
76
|
+
id: string | number;
|
|
77
|
+
__model?: { __name: string };
|
|
78
|
+
__data: Record<string, unknown> & { id?: string | number; __pendingSqlId?: boolean };
|
|
79
|
+
__relationships: Record<string, unknown>;
|
|
80
|
+
toJSON?(options?: { fields?: Set<string>; baseUrl?: string }): Record<string, unknown>;
|
|
81
|
+
[key: string]: unknown;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ForeignKeyDef {
|
|
85
|
+
references: string;
|
|
86
|
+
column: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ModelSchema {
|
|
90
|
+
table: string;
|
|
91
|
+
idType: string;
|
|
92
|
+
columns: Record<string, string>;
|
|
93
|
+
foreignKeys: Record<string, ForeignKeyDef>;
|
|
94
|
+
relationships: {
|
|
95
|
+
belongsTo: Record<string, string | null>;
|
|
96
|
+
hasMany: Record<string, string | null>;
|
|
97
|
+
};
|
|
98
|
+
vectorColumns?: Record<string, number>;
|
|
99
|
+
memory: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ViewSchema {
|
|
103
|
+
viewName: string;
|
|
104
|
+
source: string;
|
|
105
|
+
groupBy?: string;
|
|
106
|
+
columns: Record<string, string>;
|
|
107
|
+
foreignKeys: Record<string, ForeignKeyDef>;
|
|
108
|
+
aggregates: Record<string, AggregateProperty>;
|
|
109
|
+
relationships: {
|
|
110
|
+
belongsTo: Record<string, string | null>;
|
|
111
|
+
hasMany: Record<string, string | null>;
|
|
112
|
+
};
|
|
113
|
+
isView: boolean;
|
|
114
|
+
memory: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Typed relationship registry maps.
|
|
119
|
+
* Each key in Orm.relationships stores a different nested Map structure.
|
|
120
|
+
*/
|
|
121
|
+
/** Relationship registry map types — source → target → recordId → value */
|
|
122
|
+
export type HasManyMap = Map<string, Map<string, Map<unknown, unknown[]>>>;
|
|
123
|
+
export type BelongsToMap = Map<string, Map<string, Map<unknown, unknown>>>;
|
|
124
|
+
export type GlobalMap = Map<string, unknown[][]>;
|
|
125
|
+
export type PendingMap = Map<string, Map<unknown, unknown[][]>>;
|
|
126
|
+
export type PendingBelongsToMap = Map<string, Map<unknown, unknown[]>>;
|
|
127
|
+
|
|
128
|
+
export interface RelationshipMaps {
|
|
129
|
+
hasMany: HasManyMap;
|
|
130
|
+
belongsTo: BelongsToMap;
|
|
131
|
+
global: GlobalMap;
|
|
132
|
+
pending: PendingMap;
|
|
133
|
+
pendingBelongsTo: PendingBelongsToMap;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface SnapshotEntry {
|
|
137
|
+
table?: string;
|
|
138
|
+
idType?: string;
|
|
139
|
+
columns?: Record<string, string>;
|
|
140
|
+
foreignKeys?: Record<string, ForeignKeyDef>;
|
|
141
|
+
vectorColumns?: Record<string, number>;
|
|
142
|
+
isView?: boolean;
|
|
143
|
+
viewName?: string;
|
|
144
|
+
source?: string;
|
|
145
|
+
viewQuery?: string;
|
|
146
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
declare module 'pg' {
|
|
2
|
+
interface PoolConfig {
|
|
3
|
+
host?: string;
|
|
4
|
+
user?: string;
|
|
5
|
+
password?: string;
|
|
6
|
+
database?: string;
|
|
7
|
+
port?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
idleTimeoutMillis?: number;
|
|
10
|
+
connectionTimeoutMillis?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type RowData = Record<string, string | number | boolean | null>;
|
|
14
|
+
|
|
15
|
+
interface QueryResult {
|
|
16
|
+
rows: RowData[];
|
|
17
|
+
rowCount: number;
|
|
18
|
+
fields?: { name: string; dataTypeID: number }[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class Pool {
|
|
22
|
+
constructor(config?: PoolConfig);
|
|
23
|
+
query(sql: string, params?: unknown[]): Promise<QueryResult>;
|
|
24
|
+
connect(): Promise<PoolClient>;
|
|
25
|
+
end(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class PoolClient {
|
|
29
|
+
query(sql: string, params?: unknown[]): Promise<QueryResult>;
|
|
30
|
+
release(): void;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare module '@stonyx/rest-server' {
|
|
2
|
+
export class Request {
|
|
3
|
+
constructor(...args: unknown[]);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface RouteOptions {
|
|
7
|
+
name: string;
|
|
8
|
+
options?: { model: string; access: (request: unknown) => unknown } | Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default class RestServer {
|
|
12
|
+
static instance: RestServer;
|
|
13
|
+
static close(): void;
|
|
14
|
+
mountRoute(RequestClass: typeof Request, options: RouteOptions): void;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
declare module '@stonyx/utils/file' {
|
|
2
|
+
export function createFile(path: string, data: string | Record<string, unknown> | unknown[], options?: { json?: boolean }): Promise<void>;
|
|
3
|
+
export function createDirectory(path: string): Promise<void>;
|
|
4
|
+
export function updateFile(path: string, data: string | Record<string, unknown> | unknown[], options?: { json?: boolean }): Promise<void>;
|
|
5
|
+
export function readFile(path: string, options?: { json?: boolean; missingFileCallback?: () => Promise<Record<string, unknown>> }): Promise<string | Record<string, unknown> | unknown[]>;
|
|
6
|
+
export function fileExists(path: string): Promise<boolean>;
|
|
7
|
+
export function deleteDirectory(path: string): Promise<void>;
|
|
8
|
+
export function forEachFileImport(
|
|
9
|
+
path: string,
|
|
10
|
+
callback: (exported: Function | Record<string, unknown>, meta: { name: string }) => void | unknown,
|
|
11
|
+
options?: { ignoreAccessFailure?: boolean; rawName?: boolean; recursive?: boolean; recursiveNaming?: boolean }
|
|
12
|
+
): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare module '@stonyx/utils/string' {
|
|
16
|
+
export function pluralize(word: string): string;
|
|
17
|
+
export function kebabCaseToPascalCase(str: string): string;
|
|
18
|
+
export function camelCaseToKebabCase(str: string): string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare module '@stonyx/utils/object' {
|
|
22
|
+
export function get(obj: Record<string, unknown>, path: string): unknown;
|
|
23
|
+
export function getOrSet<T>(map: Map<unknown, T>, key: unknown, defaultValue: T): T;
|
|
24
|
+
export function makeArray<T>(value: T | T[]): T[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare module '@stonyx/utils/date' {
|
|
28
|
+
export function getTimestamp(value?: string | number | Date): number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare module '@stonyx/utils/prompt' {
|
|
32
|
+
export function confirm(message: string): Promise<boolean>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
declare module 'stonyx/config' {
|
|
2
|
+
import type { OrmConfig } from './orm-types.js';
|
|
3
|
+
const config: OrmConfig;
|
|
4
|
+
export default config;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
declare module 'stonyx/log' {
|
|
8
|
+
const log: Record<string, ((...args: unknown[]) => void) | undefined>;
|
|
9
|
+
export default log;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare module 'stonyx' {
|
|
13
|
+
export function waitForModule(name: string): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare module 'stonyx/test-helpers' {
|
|
17
|
+
export function setupIntegrationTests(hooks: {
|
|
18
|
+
before(fn: () => void | Promise<void>): void;
|
|
19
|
+
after(fn: () => void | Promise<void>): void;
|
|
20
|
+
}): void;
|
|
21
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { pluralize as basePluralize } from '@stonyx/utils/string';
|
|
2
|
+
import type { OrmRecord } from './types/orm-types.js';
|
|
3
|
+
|
|
4
|
+
export function isDbError(error: unknown): error is { code: string; message: string } {
|
|
5
|
+
return typeof error === 'object' && error !== null && 'code' in error && typeof (error as Record<string, unknown>).code === 'string' && 'message' in error && typeof (error as Record<string, unknown>).message === 'string';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isOrmRecord(value: unknown): value is OrmRecord {
|
|
9
|
+
return typeof value === 'object' && value !== null && '__data' in value && '__relationships' in value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Wrapper to handle dasherized model names (e.g., "access-link" → "access-links")
|
|
13
|
+
export function pluralize(word: string): string {
|
|
14
|
+
if (word.includes('-')) {
|
|
15
|
+
const parts = word.split('-');
|
|
16
|
+
const last = parts.pop() as string;
|
|
17
|
+
const pluralizedLast = basePluralize(last);
|
|
18
|
+
return [...parts, pluralizedLast].join('-');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return basePluralize(word);
|
|
22
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import Orm, { store } from '@stonyx/orm';
|
|
2
|
+
import { createRecord } from './manage-record.js';
|
|
3
|
+
import { AggregateProperty } from './aggregates.js';
|
|
4
|
+
import { get } from '@stonyx/utils/object';
|
|
5
|
+
import type { SourceRecord } from './types/orm-types.js';
|
|
6
|
+
|
|
7
|
+
interface ViewClass {
|
|
8
|
+
source?: string;
|
|
9
|
+
resolve?: Record<string, unknown>;
|
|
10
|
+
groupBy?: string;
|
|
11
|
+
new (viewName: string): Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default class ViewResolver {
|
|
15
|
+
viewName: string;
|
|
16
|
+
|
|
17
|
+
constructor(viewName: string) {
|
|
18
|
+
this.viewName = viewName;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async resolveAll(): Promise<unknown[]> {
|
|
22
|
+
const orm = Orm.instance;
|
|
23
|
+
const { modelClass: viewClass } = orm.getRecordClasses(this.viewName) as { modelClass: ViewClass | undefined; serializerClass: unknown };
|
|
24
|
+
|
|
25
|
+
if (!viewClass) return [];
|
|
26
|
+
|
|
27
|
+
const source = viewClass.source;
|
|
28
|
+
if (!source) return [];
|
|
29
|
+
|
|
30
|
+
const sourceRecords = await store.findAll(source) as SourceRecord[];
|
|
31
|
+
if (!sourceRecords || sourceRecords.length === 0) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const resolveMap = viewClass.resolve || {};
|
|
36
|
+
const viewInstance = new viewClass(this.viewName);
|
|
37
|
+
const aggregateFields: Record<string, AggregateProperty> = {};
|
|
38
|
+
const regularFields: Record<string, unknown> = {};
|
|
39
|
+
|
|
40
|
+
// Categorize fields on the view instance
|
|
41
|
+
for (const [key, value] of Object.entries(viewInstance)) {
|
|
42
|
+
if (key.startsWith('__')) continue;
|
|
43
|
+
if (key === 'id') continue;
|
|
44
|
+
|
|
45
|
+
if (value instanceof AggregateProperty) {
|
|
46
|
+
aggregateFields[key] = value;
|
|
47
|
+
} else if (typeof value !== 'function') {
|
|
48
|
+
// Regular attr or direct value — not a relationship handler
|
|
49
|
+
regularFields[key] = value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const groupByField = viewClass.groupBy;
|
|
54
|
+
|
|
55
|
+
if (groupByField) {
|
|
56
|
+
return this._resolveGroupBy(sourceRecords, groupByField, aggregateFields, regularFields, resolveMap, viewClass);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return this._resolvePerRecord(sourceRecords, aggregateFields, regularFields, resolveMap, viewClass);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private _resolvePerRecord(
|
|
63
|
+
sourceRecords: SourceRecord[],
|
|
64
|
+
aggregateFields: Record<string, AggregateProperty>,
|
|
65
|
+
regularFields: Record<string, unknown>,
|
|
66
|
+
resolveMap: Record<string, unknown>,
|
|
67
|
+
viewClass: ViewClass
|
|
68
|
+
): unknown[] {
|
|
69
|
+
const results: unknown[] = [];
|
|
70
|
+
|
|
71
|
+
for (const sourceRecord of sourceRecords) {
|
|
72
|
+
const rawData: Record<string, unknown> = { id: sourceRecord.id };
|
|
73
|
+
|
|
74
|
+
// Compute aggregate fields from source record's relationships
|
|
75
|
+
for (const [key, aggProp] of Object.entries(aggregateFields)) {
|
|
76
|
+
if (!aggProp.relationship) continue;
|
|
77
|
+
const relatedRecords = sourceRecord.__relationships?.[aggProp.relationship]
|
|
78
|
+
|| sourceRecord[aggProp.relationship];
|
|
79
|
+
const relArray = Array.isArray(relatedRecords) ? relatedRecords : [];
|
|
80
|
+
rawData[key] = aggProp.compute(relArray);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Apply resolve map entries
|
|
84
|
+
for (const [key, resolver] of Object.entries(resolveMap)) {
|
|
85
|
+
if (typeof resolver === 'function') {
|
|
86
|
+
rawData[key] = resolver(sourceRecord);
|
|
87
|
+
} else if (typeof resolver === 'string') {
|
|
88
|
+
rawData[key] = get(sourceRecord.__data || sourceRecord, resolver)
|
|
89
|
+
?? get(sourceRecord, resolver);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Map regular attr fields from source record if not already set
|
|
94
|
+
for (const key of Object.keys(regularFields)) {
|
|
95
|
+
if (rawData[key] !== undefined) continue;
|
|
96
|
+
|
|
97
|
+
const sourceValue = sourceRecord.__data?.[key] ?? sourceRecord[key];
|
|
98
|
+
if (sourceValue !== undefined) {
|
|
99
|
+
rawData[key] = sourceValue;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Set belongsTo source relationship
|
|
104
|
+
const viewInstanceForRel = new viewClass(this.viewName);
|
|
105
|
+
for (const [key, value] of Object.entries(viewInstanceForRel)) {
|
|
106
|
+
if (typeof value === 'function' && key !== 'id') {
|
|
107
|
+
// This is a relationship handler — pass the source record id
|
|
108
|
+
rawData[key] = sourceRecord.id;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Clear existing record from store to allow re-resolution
|
|
113
|
+
const viewStore = store.get(this.viewName);
|
|
114
|
+
if (viewStore?.has(rawData.id as number | string)) {
|
|
115
|
+
viewStore.delete(rawData.id as number | string);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const record = createRecord(this.viewName, rawData, { isDbRecord: true });
|
|
119
|
+
results.push(record);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private _resolveGroupBy(
|
|
126
|
+
sourceRecords: SourceRecord[],
|
|
127
|
+
groupByField: string,
|
|
128
|
+
aggregateFields: Record<string, AggregateProperty>,
|
|
129
|
+
regularFields: Record<string, unknown>,
|
|
130
|
+
resolveMap: Record<string, unknown>,
|
|
131
|
+
viewClass: ViewClass
|
|
132
|
+
): unknown[] {
|
|
133
|
+
// Group source records by the groupBy field value
|
|
134
|
+
const groups = new Map<unknown, SourceRecord[]>();
|
|
135
|
+
for (const record of sourceRecords) {
|
|
136
|
+
const key = record.__data?.[groupByField] ?? record[groupByField];
|
|
137
|
+
if (!groups.has(key)) {
|
|
138
|
+
groups.set(key, []);
|
|
139
|
+
}
|
|
140
|
+
const group = groups.get(key);
|
|
141
|
+
if (group) group.push(record);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const results: unknown[] = [];
|
|
145
|
+
|
|
146
|
+
for (const [groupKey, groupRecords] of groups) {
|
|
147
|
+
const rawData: Record<string, unknown> = { id: groupKey };
|
|
148
|
+
|
|
149
|
+
// Compute aggregate fields
|
|
150
|
+
for (const [key, aggProp] of Object.entries(aggregateFields)) {
|
|
151
|
+
if (aggProp.relationship === undefined) {
|
|
152
|
+
// Field-level aggregate — compute over group records directly
|
|
153
|
+
rawData[key] = aggProp.compute(groupRecords);
|
|
154
|
+
} else {
|
|
155
|
+
// Relationship aggregate — flatten related records across all group members
|
|
156
|
+
if (!aggProp.relationship) continue;
|
|
157
|
+
const allRelated: unknown[] = [];
|
|
158
|
+
for (const record of groupRecords) {
|
|
159
|
+
const relatedRecords = record.__relationships?.[aggProp.relationship]
|
|
160
|
+
|| record[aggProp.relationship];
|
|
161
|
+
if (Array.isArray(relatedRecords)) {
|
|
162
|
+
allRelated.push(...relatedRecords);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
rawData[key] = aggProp.compute(allRelated as { __data?: Record<string, unknown>; [key: string]: unknown }[]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Apply resolve map entries — functions receive the group array
|
|
170
|
+
for (const [key, resolver] of Object.entries(resolveMap)) {
|
|
171
|
+
if (typeof resolver === 'function') {
|
|
172
|
+
rawData[key] = resolver(groupRecords);
|
|
173
|
+
} else if (typeof resolver === 'string') {
|
|
174
|
+
// String path — take value from first record in group
|
|
175
|
+
const first = groupRecords[0];
|
|
176
|
+
rawData[key] = get(first.__data || first, resolver)
|
|
177
|
+
?? get(first, resolver);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Map regular attr fields from first record if not already set
|
|
182
|
+
for (const key of Object.keys(regularFields)) {
|
|
183
|
+
if (rawData[key] !== undefined) continue;
|
|
184
|
+
const first = groupRecords[0];
|
|
185
|
+
const sourceValue = first.__data?.[key] ?? first[key];
|
|
186
|
+
if (sourceValue !== undefined) {
|
|
187
|
+
rawData[key] = sourceValue;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Clear existing record from store to allow re-resolution
|
|
192
|
+
const viewStore = store.get(this.viewName);
|
|
193
|
+
if (viewStore?.has(rawData.id as number | string)) {
|
|
194
|
+
viewStore.delete(rawData.id as number | string);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const record = createRecord(this.viewName, rawData, { isDbRecord: true });
|
|
198
|
+
results.push(record);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async resolveOne(id: unknown): Promise<unknown> {
|
|
205
|
+
const all = await this.resolveAll();
|
|
206
|
+
return all.find((record: unknown) => {
|
|
207
|
+
const r = record as SourceRecord;
|
|
208
|
+
return r.id === id || r.id == id;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/view.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import attr from './attr.js';
|
|
2
|
+
|
|
3
|
+
export default class View {
|
|
4
|
+
static memory: boolean = false;
|
|
5
|
+
static readOnly: boolean = true;
|
|
6
|
+
static pluralName: string | undefined = undefined;
|
|
7
|
+
static source: string | undefined = undefined;
|
|
8
|
+
static groupBy: string | undefined = undefined;
|
|
9
|
+
static resolve: ((record: unknown) => unknown) | undefined = undefined;
|
|
10
|
+
|
|
11
|
+
id = attr('number');
|
|
12
|
+
__name: string;
|
|
13
|
+
|
|
14
|
+
constructor(name: string) {
|
|
15
|
+
this.__name = name;
|
|
16
|
+
|
|
17
|
+
// Enforce readOnly — cannot be overridden to false
|
|
18
|
+
if ((this.constructor as typeof View).readOnly !== true) {
|
|
19
|
+
throw new Error(`View '${name}' cannot override readOnly to false`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# Stonyx Code Style Rules
|
|
2
|
-
|
|
3
|
-
Strict prettier/eslint rules to apply across all Stonyx projects. These will be formalized into an ESLint/Prettier config once enough patterns are collected.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Rules
|
|
8
|
-
|
|
9
|
-
### 1. Destructure config objects in function signatures
|
|
10
|
-
|
|
11
|
-
When a function receives a config/options object and only uses specific properties, destructure them in the function signature rather than accessing them via dot notation in the body.
|
|
12
|
-
|
|
13
|
-
**Bad:**
|
|
14
|
-
```javascript
|
|
15
|
-
export async function getPool(mysqlConfig) {
|
|
16
|
-
pool = mysql.createPool({
|
|
17
|
-
host: mysqlConfig.host,
|
|
18
|
-
port: mysqlConfig.port,
|
|
19
|
-
user: mysqlConfig.user,
|
|
20
|
-
password: mysqlConfig.password,
|
|
21
|
-
database: mysqlConfig.database,
|
|
22
|
-
connectionLimit: mysqlConfig.connectionLimit,
|
|
23
|
-
// ...
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
**Good:**
|
|
29
|
-
```javascript
|
|
30
|
-
export async function getPool({ host, port, user, password, database, connectionLimit }) {
|
|
31
|
-
pool = mysql.createPool({
|
|
32
|
-
host,
|
|
33
|
-
port,
|
|
34
|
-
user,
|
|
35
|
-
password,
|
|
36
|
-
database,
|
|
37
|
-
connectionLimit,
|
|
38
|
-
// ...
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
**Source:** PR #14, `src/mysql/connection.js`
|
|
44
|
-
**ESLint rule (candidate):** `prefer-destructuring` (with custom config for function parameters)
|