arckode-framework 1.3.2 → 1.4.0

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.
Files changed (64) hide show
  1. package/adapters/jwt.ts +6 -4
  2. package/adapters/mysql.ts +7 -2
  3. package/adapters/postgres.ts +37 -0
  4. package/adapters/sqlite.ts +7 -1
  5. package/adapters/vendor.d.ts +48 -0
  6. package/cli/analyze/checks.ts +333 -0
  7. package/cli/analyze/index.ts +44 -0
  8. package/cli/analyze/report.ts +107 -0
  9. package/cli/analyze/types.ts +46 -0
  10. package/cli/analyze/utils.ts +36 -0
  11. package/cli/analyze.ts +2 -647
  12. package/cli/commands/db-migrate.ts +213 -89
  13. package/cli/commands/db-seed.ts +97 -32
  14. package/cli/commands/db-utils.ts +192 -0
  15. package/cli/commands/new.ts +175 -0
  16. package/cli/commands/routes.ts +94 -0
  17. package/cli/index.ts +57 -404
  18. package/cli/stubs/module/core.ts +162 -0
  19. package/cli/stubs/module/data.ts +171 -0
  20. package/cli/stubs/module/index.ts +5 -0
  21. package/cli/stubs/module/service.ts +198 -0
  22. package/cli/stubs/module/types.ts +12 -0
  23. package/cli/stubs/module-stub.ts +2 -552
  24. package/kernel/auth.ts +114 -0
  25. package/kernel/cache.ts +37 -0
  26. package/kernel/config.ts +129 -0
  27. package/kernel/container.ts +64 -0
  28. package/kernel/db/orm-migrate.ts +136 -0
  29. package/kernel/db/orm-repository.ts +45 -0
  30. package/kernel/db/orm-utils.ts +93 -0
  31. package/kernel/db/orm.ts +254 -0
  32. package/kernel/db/transactor.ts +17 -0
  33. package/kernel/db/types.ts +72 -0
  34. package/kernel/errors.ts +102 -0
  35. package/kernel/framework.default.ts +41 -0
  36. package/kernel/framework.ts +8 -2144
  37. package/kernel/http/router.ts +131 -0
  38. package/kernel/http/server.ts +303 -0
  39. package/kernel/http/types.ts +56 -0
  40. package/kernel/index.ts +25 -0
  41. package/kernel/logger.ts +50 -0
  42. package/kernel/middlewares.ts +19 -7
  43. package/kernel/modules/create-module.ts +5 -0
  44. package/kernel/modules/system.ts +149 -0
  45. package/kernel/modules/types.ts +46 -0
  46. package/kernel/seeds.ts +48 -0
  47. package/kernel/static.ts +11 -2
  48. package/kernel/testing.ts +8 -3
  49. package/kernel/validator.ts +116 -0
  50. package/modules/events/index.ts +19 -3
  51. package/modules/mail/index.ts +14 -2
  52. package/modules/storage/local-adapter.ts +19 -5
  53. package/modules/ws/index.ts +123 -18
  54. package/package.json +8 -11
  55. package/skills/auth/SKILL.md +36 -220
  56. package/skills/cli/SKILL.md +32 -251
  57. package/skills/config/SKILL.md +30 -239
  58. package/skills/connectors/SKILL.md +32 -295
  59. package/skills/helpers/SKILL.md +26 -195
  60. package/skills/middlewares/SKILL.md +30 -280
  61. package/skills/orm/SKILL.md +42 -349
  62. package/skills/realtime/SKILL.md +22 -297
  63. package/skills/services/SKILL.md +40 -183
  64. package/skills/testing/SKILL.md +34 -266
@@ -0,0 +1,254 @@
1
+ import { InternalError, NotFoundError, ValidationError } from '../errors'
2
+ import type { DbAdapter, FindOptions, ModelDefinition, ModelResult, PageResult } from './types'
3
+ import {
4
+ assertSafeIdentifier,
5
+ serializeForDb,
6
+ deserializeFromDb,
7
+ getAllowedFields,
8
+ buildSelect,
9
+ buildWhere,
10
+ } from './orm-utils'
11
+ import { ormMigrate } from './orm-migrate'
12
+
13
+ export class ORM {
14
+ private models = new Map<string, ModelDefinition>()
15
+
16
+ static readonly SCHEMA_TABLE = '_arckode_schema'
17
+
18
+ constructor(private db: DbAdapter) {}
19
+
20
+ define(name: string, def: ModelDefinition): this {
21
+ assertSafeIdentifier(def.table, `modelo "${name}" → table`)
22
+ for (const fieldName of Object.keys(def.fields)) {
23
+ assertSafeIdentifier(fieldName, `modelo "${name}" → campo`)
24
+ }
25
+ this.models.set(name, def)
26
+ return this
27
+ }
28
+
29
+ getDefinition(name: string): ModelDefinition {
30
+ const def = this.models.get(name)
31
+ if (!def) throw new NotFoundError(`Modelo "${name}" no definido`)
32
+ return def
33
+ }
34
+
35
+ async transaction<T>(fn: (tx: ORM) => Promise<T>): Promise<T> {
36
+ if (this.db.transaction) {
37
+ return this.db.transaction(async (txAdapter) => {
38
+ const txOrm = new ORM(txAdapter)
39
+ for (const [name, def] of this.models) txOrm.define(name, def)
40
+ return fn(txOrm)
41
+ })
42
+ }
43
+ throw new InternalError('El adaptador de base de datos no soporta transacciones. Usá SqliteAdapter, MySQLAdapter o PostgresAdapter.')
44
+ }
45
+
46
+ // ── CRUD ──
47
+
48
+ async findMany<T extends object = ModelResult>(name: string, filters?: Record<string, unknown>, options?: FindOptions): Promise<T[]> {
49
+ const def = this.getDefinition(name)
50
+ const { clause, params } = buildWhere(def, filters)
51
+ const selectClause = buildSelect(def, options?.select)
52
+ let sql = `SELECT ${selectClause} FROM ${def.table}${clause}`
53
+
54
+ if (options?.orderBy) {
55
+ const allowed = getAllowedFields(def)
56
+ const clauses = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy]
57
+ const parts = clauses.map(({ field, dir = 'ASC' }) => {
58
+ if (!allowed.has(field)) throw new ValidationError(`Invalid sort field: "${field}"`)
59
+ return `${field} ${dir === 'DESC' ? 'DESC' : 'ASC'}`
60
+ })
61
+ sql += ` ORDER BY ${parts.join(', ')}`
62
+ }
63
+
64
+ if (options?.limit !== undefined) {
65
+ sql += ` LIMIT ${Math.max(1, Math.floor(options.limit))}`
66
+ if (options.offset !== undefined) {
67
+ sql += ` OFFSET ${Math.max(0, Math.floor(options.offset))}`
68
+ }
69
+ }
70
+
71
+ const rows = (await this.db.query(sql, params)) as T[]
72
+ return rows.map(r => deserializeFromDb(def, r as ModelResult) as T)
73
+ }
74
+
75
+ async findById<T extends object = ModelResult>(name: string, id: string, select?: string[]): Promise<T | null> {
76
+ const def = this.getDefinition(name)
77
+ const selectClause = buildSelect(def, select)
78
+ let sql = `SELECT ${selectClause} FROM ${def.table} WHERE id = ?`
79
+ if (def.softDelete) sql += ' AND deletedAt IS NULL'
80
+ const rows = await this.db.query(sql, [id])
81
+ const row = (rows as ModelResult[])[0] ?? null
82
+ return row ? deserializeFromDb(def, row) as T : null
83
+ }
84
+
85
+ async findOne<T extends object = ModelResult>(name: string, filters: Record<string, unknown>): Promise<T | null> {
86
+ const results = await this.findMany<T>(name, filters, { limit: 1 })
87
+ return results[0] ?? null
88
+ }
89
+
90
+ async count(name: string, filters?: Record<string, unknown>): Promise<number> {
91
+ const def = this.getDefinition(name)
92
+ const { clause, params } = buildWhere(def, filters)
93
+ const rows = await this.db.query(`SELECT COUNT(*) as n FROM ${def.table}${clause}`, params)
94
+ return Number((rows as Array<{ n: number | string }>)[0]?.n ?? 0)
95
+ }
96
+
97
+ async paginate<T extends object = ModelResult>(
98
+ name: string,
99
+ filters?: Record<string, unknown>,
100
+ options: FindOptions & { limit: number } = { limit: 20 },
101
+ ): Promise<PageResult<T>> {
102
+ const limit = Math.max(1, Math.floor(options.limit))
103
+ const offset = Math.max(0, Math.floor(options.offset ?? 0))
104
+
105
+ const [data, total] = await Promise.all([
106
+ this.findMany<T>(name, filters, { ...options, limit, offset }),
107
+ this.count(name, filters),
108
+ ])
109
+
110
+ return { data, total, limit, offset, pages: Math.ceil(total / limit) }
111
+ }
112
+
113
+ async create<T extends object = ModelResult>(name: string, data: Record<string, unknown>): Promise<T> {
114
+ const def = this.getDefinition(name)
115
+ const allowedFields = new Set(Object.keys(def.fields))
116
+ const id = crypto.randomUUID()
117
+ const now = new Date().toISOString()
118
+
119
+ const record: Record<string, unknown> = { id }
120
+ for (const [k, v] of Object.entries(data)) {
121
+ if (allowedFields.has(k)) record[k] = v
122
+ }
123
+ if (def.timestamps) {
124
+ record.createdAt = now
125
+ record.updatedAt = now
126
+ }
127
+
128
+ const dbRecord = serializeForDb(def, record)
129
+ const keys = Object.keys(dbRecord)
130
+ const values = Object.values(dbRecord)
131
+ const placeholders = keys.map(() => '?').join(', ')
132
+
133
+ await this.db.run(
134
+ `INSERT INTO ${def.table} (${keys.join(', ')}) VALUES (${placeholders})`,
135
+ values,
136
+ )
137
+
138
+ return record as T
139
+ }
140
+
141
+ async update<T extends object = ModelResult>(name: string, id: string, data: Record<string, unknown>): Promise<T | null> {
142
+ const def = this.getDefinition(name)
143
+ const allowedFields = new Set(Object.keys(def.fields))
144
+ const now = new Date().toISOString()
145
+
146
+ const record: Record<string, unknown> = {}
147
+ for (const [k, v] of Object.entries(data)) {
148
+ if (allowedFields.has(k)) record[k] = v
149
+ }
150
+ if (def.timestamps) record.updatedAt = now
151
+
152
+ const dbRecord = serializeForDb(def, record)
153
+ const keys = Object.keys(dbRecord)
154
+ if (keys.length === 0) return this.findById<T>(name, id)
155
+ const values = Object.values(dbRecord)
156
+ const setClause = keys.map(k => `${k} = ?`).join(', ')
157
+
158
+ await this.db.run(`UPDATE ${def.table} SET ${setClause} WHERE id = ?`, [...values, id])
159
+ return this.findById<T>(name, id)
160
+ }
161
+
162
+ async delete(name: string, id: string): Promise<boolean> {
163
+ const def = this.getDefinition(name)
164
+
165
+ if (def.softDelete) {
166
+ const result = await this.db.run(`UPDATE ${def.table} SET deletedAt = ? WHERE id = ?`, [new Date().toISOString(), id])
167
+ return result.changes > 0
168
+ }
169
+
170
+ const result = await this.db.run(`DELETE FROM ${def.table} WHERE id = ?`, [id])
171
+ return result.changes > 0
172
+ }
173
+
174
+ // ── Bulk operations ──
175
+
176
+ async createMany(name: string, records: Record<string, unknown>[]): Promise<ModelResult[]> {
177
+ if (records.length === 0) return []
178
+ const def = this.getDefinition(name)
179
+ const allowedFields = new Set(Object.keys(def.fields))
180
+ const now = new Date().toISOString()
181
+
182
+ const prepared = records.map(data => {
183
+ const record: Record<string, unknown> = { id: crypto.randomUUID() }
184
+ for (const [k, v] of Object.entries(data)) {
185
+ if (allowedFields.has(k)) record[k] = v
186
+ }
187
+ if (def.timestamps) { record.createdAt = now; record.updatedAt = now }
188
+ return record
189
+ })
190
+
191
+ const dbPrepared = prepared.map(r => serializeForDb(def, r))
192
+ const keys = Object.keys(dbPrepared[0] ?? {})
193
+ const rowPlaceholders = `(${keys.map(() => '?').join(', ')})`
194
+ const allPlaceholders = dbPrepared.map(() => rowPlaceholders).join(', ')
195
+ const allValues = dbPrepared.flatMap(r => Object.values(r))
196
+
197
+ await this.db.run(
198
+ `INSERT INTO ${def.table} (${keys.join(', ')}) VALUES ${allPlaceholders}`,
199
+ allValues,
200
+ )
201
+
202
+ return prepared as ModelResult[]
203
+ }
204
+
205
+ async updateMany(
206
+ name: string,
207
+ filters: Record<string, unknown>,
208
+ changes: Record<string, unknown>,
209
+ ): Promise<number> {
210
+ const def = this.getDefinition(name)
211
+ const allowedFields = new Set(Object.keys(def.fields))
212
+ const { clause, params: whereParams } = buildWhere(def, filters)
213
+ const now = new Date().toISOString()
214
+
215
+ const data: Record<string, unknown> = {}
216
+ for (const [k, v] of Object.entries(changes)) {
217
+ if (allowedFields.has(k)) data[k] = v
218
+ }
219
+ if (def.timestamps) data.updatedAt = now
220
+
221
+ const dbData = serializeForDb(def, data)
222
+ const setClause = Object.keys(dbData).map(k => `${k} = ?`).join(', ')
223
+ const result = await this.db.run(
224
+ `UPDATE ${def.table} SET ${setClause}${clause}`,
225
+ [...Object.values(dbData), ...whereParams],
226
+ )
227
+ return result.changes
228
+ }
229
+
230
+ async deleteMany(name: string, filters: Record<string, unknown>): Promise<number> {
231
+ if (Object.keys(filters).length === 0) {
232
+ throw new ValidationError('deleteMany requires at least one filter to avoid deleting the entire table')
233
+ }
234
+ const def = this.getDefinition(name)
235
+ const { clause, params } = buildWhere(def, filters)
236
+
237
+ if (def.softDelete) {
238
+ const result = await this.db.run(
239
+ `UPDATE ${def.table} SET deletedAt = ?${clause}`,
240
+ [new Date().toISOString(), ...params],
241
+ )
242
+ return result.changes
243
+ }
244
+
245
+ const result = await this.db.run(`DELETE FROM ${def.table}${clause}`, params)
246
+ return result.changes
247
+ }
248
+
249
+ // ── Migraciones ──
250
+
251
+ async migrate(opts: { allowDrop?: boolean } = {}): Promise<void> {
252
+ return ormMigrate(this.db, this.models, opts)
253
+ }
254
+ }
@@ -0,0 +1,17 @@
1
+ import type { RepositoryAdapter, Transactor, TransactionalRepos } from './types'
2
+ import { ORM } from './orm'
3
+ import { OrmRepository } from './orm-repository'
4
+
5
+ export class OrmTransactor implements Transactor {
6
+ constructor(private readonly orm: ORM) {}
7
+
8
+ run<R>(fn: (repos: TransactionalRepos) => Promise<R>): Promise<R> {
9
+ return this.orm.transaction(async (txOrm) => {
10
+ const repos: TransactionalRepos = {
11
+ for: <T extends object = Record<string, unknown>>(modelName: string): RepositoryAdapter<T> =>
12
+ new OrmRepository<T>(txOrm, modelName),
13
+ }
14
+ return fn(repos)
15
+ })
16
+ }
17
+ }
@@ -0,0 +1,72 @@
1
+ export interface DbAdapter {
2
+ query(sql: string, params?: unknown[]): Promise<unknown[]>
3
+ run(sql: string, params?: unknown[]): Promise<{ changes: number; lastId?: string }>
4
+ close(): Promise<void>
5
+ /** Opcional: soporte de transacciones. Si no lo implementa, ORM.transaction() lanza InternalError. */
6
+ transaction?<T>(fn: (adapter: DbAdapter) => Promise<T>): Promise<T>
7
+ }
8
+
9
+ export interface FieldDefinition {
10
+ type: 'string' | 'text' | 'number' | 'boolean' | 'json' | 'date'
11
+ required?: boolean
12
+ nullable?: boolean
13
+ default?: unknown
14
+ unique?: boolean
15
+ indexed?: boolean
16
+ maxLength?: number
17
+ }
18
+
19
+ export interface ModelDefinition {
20
+ table: string
21
+ fields: Record<string, FieldDefinition>
22
+ timestamps?: boolean
23
+ softDelete?: boolean
24
+ }
25
+
26
+ export interface ModelResult {
27
+ id: string
28
+ [key: string]: unknown
29
+ }
30
+
31
+ export interface OrderByClause {
32
+ field: string
33
+ dir?: 'ASC' | 'DESC'
34
+ }
35
+
36
+ export interface FindOptions {
37
+ limit?: number
38
+ offset?: number
39
+ orderBy?: OrderByClause | OrderByClause[]
40
+ /** Campos a seleccionar. Si se omite: SELECT *. Los nombres se validan contra el modelo. */
41
+ select?: string[]
42
+ }
43
+
44
+ export interface PageResult<T = ModelResult> {
45
+ data: T[]
46
+ total: number
47
+ limit: number
48
+ offset: number
49
+ pages: number
50
+ }
51
+
52
+ export interface RepositoryAdapter<T extends object = Record<string, unknown>> {
53
+ findMany(filters?: Record<string, unknown>, options?: FindOptions): Promise<T[]>
54
+ findById(id: string, select?: string[]): Promise<T | null>
55
+ findOne(filters: Record<string, unknown>): Promise<T | null>
56
+ create(data: Omit<T, 'id'>): Promise<T>
57
+ update(id: string, data: Partial<Omit<T, 'id'>>): Promise<T | null>
58
+ delete(id: string): Promise<boolean>
59
+ count(filters?: Record<string, unknown>): Promise<number>
60
+ paginate(
61
+ filters?: Record<string, unknown>,
62
+ options?: FindOptions & { limit: number },
63
+ ): Promise<PageResult<T>>
64
+ }
65
+
66
+ export interface TransactionalRepos {
67
+ for<T extends object = Record<string, unknown>>(modelName: string): RepositoryAdapter<T>
68
+ }
69
+
70
+ export interface Transactor {
71
+ run<R>(fn: (repos: TransactionalRepos) => Promise<R>): Promise<R>
72
+ }
@@ -0,0 +1,102 @@
1
+ export abstract class ErrorContract extends Error {
2
+ public abstract readonly httpStatus: number
3
+ public abstract readonly isExpected: boolean
4
+ public abstract readonly canRetry: boolean
5
+ public abstract readonly errorCode: string
6
+
7
+ constructor(
8
+ message: string,
9
+ public readonly details?: Record<string, unknown>,
10
+ ) {
11
+ super(message)
12
+ this.name = this.constructor.name
13
+ }
14
+
15
+ toJSON(): Record<string, unknown> {
16
+ return {
17
+ error: this.message,
18
+ code: this.errorCode,
19
+ ...(this.details ? { details: this.details } : {}),
20
+ }
21
+ }
22
+ }
23
+
24
+ export class ValidationError extends ErrorContract {
25
+ httpStatus = 400
26
+ isExpected = true
27
+ canRetry = false
28
+ errorCode = 'VALIDATION_ERROR'
29
+ constructor(msg: string, public fields?: Record<string, string[]>) {
30
+ super(msg, fields ? { fields } : undefined)
31
+ }
32
+ }
33
+
34
+ export class AuthError extends ErrorContract {
35
+ httpStatus = 401
36
+ isExpected = true
37
+ canRetry = false
38
+ errorCode = 'AUTH_ERROR'
39
+ constructor(message = 'Authentication error', details?: Record<string, unknown>) {
40
+ super(message, details)
41
+ }
42
+ }
43
+
44
+ export class ForbiddenError extends ErrorContract {
45
+ httpStatus = 403
46
+ isExpected = true
47
+ canRetry = false
48
+ errorCode = 'FORBIDDEN'
49
+ }
50
+
51
+ export class NotFoundError extends ErrorContract {
52
+ httpStatus = 404
53
+ isExpected = true
54
+ canRetry = false
55
+ errorCode = 'NOT_FOUND'
56
+ }
57
+
58
+ export class ConflictError extends ErrorContract {
59
+ httpStatus = 409
60
+ isExpected = true
61
+ canRetry = false
62
+ errorCode = 'CONFLICT'
63
+ }
64
+
65
+ export class RateLimitError extends ErrorContract {
66
+ httpStatus = 429
67
+ isExpected = true
68
+ canRetry = true
69
+ errorCode = 'RATE_LIMIT'
70
+ }
71
+
72
+ export class RepositoryError extends ErrorContract {
73
+ httpStatus = 500
74
+ isExpected = true
75
+ canRetry = true
76
+ errorCode = 'REPOSITORY_ERROR'
77
+ }
78
+
79
+ export class InternalError extends ErrorContract {
80
+ httpStatus = 500
81
+ isExpected = false
82
+ canRetry = false
83
+ errorCode = 'INTERNAL_ERROR'
84
+ }
85
+
86
+ export class PayloadTooLargeError extends ErrorContract {
87
+ httpStatus = 413
88
+ isExpected = true
89
+ canRetry = false
90
+ errorCode = 'PAYLOAD_TOO_LARGE'
91
+ constructor() { super('Payload Too Large') }
92
+ }
93
+
94
+ export class ModuleRuleError extends ErrorContract {
95
+ httpStatus = 500
96
+ isExpected = false
97
+ canRetry = false
98
+ errorCode = 'MODULE_RULE_VIOLATION'
99
+ constructor(module: string, rule: string) {
100
+ super(`[${module}] Regla violada: ${rule}`, { module, rule })
101
+ }
102
+ }
@@ -0,0 +1,41 @@
1
+ import {
2
+ ErrorContract, ValidationError, AuthError, ForbiddenError,
3
+ NotFoundError, ConflictError, RateLimitError, RepositoryError,
4
+ InternalError, ModuleRuleError,
5
+ } from './errors'
6
+ import { Logger, ConsoleTransport } from './logger'
7
+ import { ConfigStore, loadEnv } from './config'
8
+ import { Container } from './container'
9
+ import { ORM, } from './db/orm'
10
+ import { OrmRepository } from './db/orm-repository'
11
+ import { OrmTransactor } from './db/transactor'
12
+ import { Router } from './http/router'
13
+ import { NodeServer } from './http/server'
14
+ import { validateSchema } from './validator'
15
+ import { Auth, } from './auth'
16
+ import { MemoryCache } from './cache'
17
+ import { createModule, } from './modules/create-module'
18
+ import { System } from './modules/system'
19
+ import { SeedRunner } from './seeds'
20
+
21
+ export default {
22
+ // Errors
23
+ ErrorContract, ValidationError, AuthError, ForbiddenError,
24
+ NotFoundError, ConflictError, RateLimitError, RepositoryError,
25
+ InternalError, ModuleRuleError,
26
+
27
+ // Infrastructure
28
+ Logger, ConsoleTransport,
29
+ ConfigStore, loadEnv,
30
+ Container,
31
+ ORM, OrmRepository, OrmTransactor,
32
+ Router, NodeServer,
33
+ validateSchema,
34
+ Auth, MemoryCache,
35
+
36
+ // Module System
37
+ createModule, System,
38
+
39
+ // Seeds
40
+ SeedRunner,
41
+ }