@akanjs/cli 0.9.60-canary.9 → 1.0.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 (26) hide show
  1. package/cjs/index.js +16 -47
  2. package/cjs/src/guidelines/scalarConstant/scalarConstant.generate.json +24 -20
  3. package/cjs/src/guidelines/scalarConstant/scalarConstant.instruction.md +284 -326
  4. package/cjs/src/guidelines/scalarDictionary/scalarDictionary.generate.json +32 -32
  5. package/cjs/src/guidelines/scalarDictionary/scalarDictionary.instruction.md +175 -249
  6. package/cjs/src/templates/app/app/[lang]/page.js +19 -36
  7. package/cjs/src/templates/workspaceRoot/package.json.template +5 -5
  8. package/esm/index.js +16 -47
  9. package/esm/src/guidelines/scalarConstant/scalarConstant.generate.json +24 -20
  10. package/esm/src/guidelines/scalarConstant/scalarConstant.instruction.md +284 -326
  11. package/esm/src/guidelines/scalarDictionary/scalarDictionary.generate.json +32 -32
  12. package/esm/src/guidelines/scalarDictionary/scalarDictionary.instruction.md +175 -249
  13. package/esm/src/templates/app/app/[lang]/page.js +19 -36
  14. package/esm/src/templates/workspaceRoot/package.json.template +5 -5
  15. package/package.json +5 -4
  16. package/src/guidelines/scalarConstant/scalarConstant.instruction.md +284 -326
  17. package/src/guidelines/scalarDictionary/scalarDictionary.instruction.md +175 -249
  18. package/src/library/library.command.d.ts +0 -2
  19. package/src/library/library.runner.d.ts +0 -2
  20. package/src/library/library.script.d.ts +0 -2
  21. package/src/scalar/scalar.command.d.ts +1 -1
  22. package/cjs/src/guidelines/fieldDecorator/fieldDecorator.generate.json +0 -135
  23. package/cjs/src/guidelines/fieldDecorator/fieldDecorator.instruction.md +0 -606
  24. package/esm/src/guidelines/fieldDecorator/fieldDecorator.generate.json +0 -135
  25. package/esm/src/guidelines/fieldDecorator/fieldDecorator.instruction.md +0 -606
  26. package/src/guidelines/fieldDecorator/fieldDecorator.instruction.md +0 -606
@@ -17,330 +17,328 @@ Scalar constants in Akan.js serve as the foundation for complex data modeling by
17
17
  ```
18
18
  {app,lib}/
19
19
  └── */lib/__scalar/
20
- └── <scalarName>/ // camelCase directory
21
- └── <scalarName>.constant.ts // scalar definition file
20
+ └── <scalarName>/ # camelCase directory
21
+ └── <scalarName>.constant.ts # scalar definition file
22
22
  ```
23
23
 
24
24
  ### Naming Standards
25
25
 
26
- - **Directory**: `camelCase` (e.g., `geoLocation`)
26
+ - **Directory**: `camelCase` (e.g., `encourageInfo`, `geoLocation`)
27
27
  - **File**: `<scalarName>.constant.ts` (matches directory name)
28
- - **Class**: `PascalCase` matching directory name (e.g., `GeoLocation`)
29
- - **Model Reference**: Matches class name in `@Model.Scalar()` decorator
30
- - **Enum Values**: `camelCase` (e.g., `active`, `pendingApproval`)
31
-
32
- ### File Structure
33
-
34
- ```typescript
35
- // 1. Core imports
36
- import { Float, Int, String, Boolean, Date, type Dayjs, dayjs, enumOf } from "@akanjs/base";
37
- import { Field, Model } from "@akanjs/constant";
38
-
39
- // 2. Optional enum definitions (camelCase values)
40
- export const Status = enumOf(["active", "inactive"] as const);
41
- export type Status = enumOf<typeof Status>;
42
-
43
- // 3. Scalar model class (SINGLE CLASS PER FILE)
44
- // Must match class name
45
- export class ScalarName {
46
- // Field definitions
47
- @Field.Prop(() => FieldType, { ...options })
48
- fieldName: FieldType;
49
- }
50
- ```
28
+ - **Scalar Class**: `PascalCase` (e.g., `EncourageInfo`, `GeoLocation`)
29
+ - **Enum Class**: `PascalCase` (e.g., `Journey`, `NotiSetting`)
30
+ - **Enum Values**: `camelCase` (e.g., `firstJoin`, `waitPay`)
51
31
 
52
32
  ## Required Imports
53
33
 
54
34
  ### Essential Framework Imports
55
35
 
56
36
  ```typescript
57
- import { Field, Model } from "@akanjs/constant";
37
+ import { via } from "@akanjs/constant";
58
38
  ```
59
39
 
60
40
  ### Common Base Types
61
41
 
62
42
  ```typescript
63
- import { ID, Int, Float, String, Boolean, Date, type Dayjs, dayjs, enumOf, JSON, Map } from "@akanjs/base";
43
+ import { ID, Int, Float, dayjs, enumOf } from "@akanjs/base";
64
44
  ```
65
45
 
66
46
  ### Cross-Scalar References
67
47
 
68
48
  ```typescript
69
- import { OtherScalar } from "../other-scalar/other-scalar.constant";
49
+ import { OtherScalar } from "../otherScalar/otherScalar.constant";
70
50
  ```
71
51
 
72
- ## Model Definition
52
+ ## Basic Syntax with via()
53
+
54
+ The `via()` function is the foundation for defining scalars. It takes a callback that receives the `field()` function, which you use to define each field's type and options.
73
55
 
74
56
  ### Basic Structure
75
57
 
76
58
  ```typescript
77
- // String must match class name
78
- export class ScalarName {
79
- // Field definitions go here
59
+ import { via } from "@akanjs/constant";
60
+
61
+ export class ScalarName extends via((field) => ({
62
+ fieldName: field(FieldType),
63
+ fieldWithOptions: field(FieldType, { ...options }),
64
+ })) {
65
+ // Optional: Add instance methods here
80
66
  }
81
67
  ```
82
68
 
83
- ### Key Rules
84
-
85
- - **One scalar model per file** - Create separate files for different scalars
86
- - **Class name matches directory name** in PascalCase (e.g., `PaymentInfo` for `payment-info`)
87
- - **Model names should be domain-specific** and descriptive
88
- - **No ID/timestamp fields** - Scalars are value objects, not entities
89
- - **`@Model.Scalar` decorator parameter must match class name** - This is critical for proper metadata registration
90
-
91
- ## Field Definitions
92
-
93
- ### Basic Field Types
69
+ ### Simple Example
94
70
 
95
71
  ```typescript
96
- @Field.Prop(() => String)
97
- name: string;
72
+ import { via } from "@akanjs/constant";
98
73
 
99
- @Field.Prop(() => Int)
100
- quantity: number;
74
+ export class RestrictInfo extends via((field) => ({
75
+ until: field(Date),
76
+ reason: field(String),
77
+ })) {}
78
+ ```
101
79
 
102
- @Field.Prop(() => Float)
103
- percentage: number;
80
+ ### Key Patterns
104
81
 
105
- @Field.Prop(() => Boolean)
106
- isActive: boolean;
82
+ | Pattern | Description |
83
+ | -------------------------- | -------------------------------------------------------------------------------- |
84
+ | `via((field) => ({...}))` | Creates a class with typed fields. The callback receives the `field()` helper. |
85
+ | `field(Type)` | Defines a single field. First argument is the type (String, Number, Date, etc.). |
86
+ | `field(Type, { options })` | Optional second argument is an options object for defaults, validation, etc. |
107
87
 
108
- @Field.Prop(() => Date)
109
- timestamp: Dayjs; // Always use Dayjs for dates
110
- ```
88
+ ## Available Field Types
111
89
 
112
- ### Field Options Reference
113
-
114
- | Option | Type | Description | Example |
115
- | ----------- | -------- | ------------------------------------ | ---------------------------- |
116
- | `default` | Any | Default field value | `{ default: 0 }` |
117
- | `nullable` | Boolean | Allows null values | `{ nullable: true }` |
118
- | `enum` | Enum | Restricts to enum values | `{ enum: Status }` |
119
- | `min` | Number | Minimum numeric value | `{ min: 0 }` |
120
- | `max` | Number | Maximum numeric value | `{ max: 100 }` |
121
- | `minlength` | Number | Minimum string length | `{ minlength: 3 }` |
122
- | `maxlength` | Number | Maximum string length | `{ maxlength: 255 }` |
123
- | `example` | Any | Example value for documentation | `{ example: [0,0] }` |
124
- | `validate` | Function | Custom validation function | `{ validate: (v) => v > 0 }` |
125
- | `immutable` | Boolean | Prevents modification after creation | `{ immutable: true }` |
126
- | `select` | Boolean | Includes in query results by default | `{ select: false }` |
127
- | `text` | String | Enables text search capabilities | `{ text: "search" }` |
128
-
129
- ### Special Field Types
90
+ ### Primitive Types
130
91
 
131
92
  ```typescript
132
- // Hidden field (not exposed in GraphQL)
133
- @Field.Hidden(() => String)
134
- internalCode: string;
135
-
136
- // Secret field (not selected by default)
137
- @Field.Secret(() => String)
138
- apiKey: string;
139
-
140
- // Resolve field (computed at runtime)
141
- @Field.Resolve(() => Int)
142
- get total(): number {
143
- return this.items.length;
144
- }
145
- ```
93
+ import { ID, Int, Float } from "@akanjs/base";
94
+ import { via } from "@akanjs/constant";
146
95
 
147
- ## Array Fields
96
+ export class Example extends via((field) => ({
97
+ // String type
98
+ name: field(String),
148
99
 
149
- ### Simple Arrays
100
+ // Number types
101
+ count: field(Int), // Integer
102
+ price: field(Float), // Floating point
150
103
 
151
- ```typescript
152
- // String array
153
- @Field.Prop(() => [String])
154
- tags: string[];
104
+ // Boolean type
105
+ isActive: field(Boolean),
155
106
 
156
- // Number array with default
157
- @Field.Prop(() => [Int], { default: [1, 2, 3] })
158
- values: number[];
107
+ // Date type (internally uses Dayjs)
108
+ createdAt: field(Date),
109
+
110
+ // ID type (MongoDB ObjectId)
111
+ referenceId: field(ID),
112
+ })) {}
159
113
  ```
160
114
 
161
- ### Nested Arrays
115
+ ### Array Types
116
+
117
+ Array types are defined by wrapping the type in square brackets:
162
118
 
163
119
  ```typescript
164
- // 2D number array
165
- @Field.Prop(() => [[Int]])
166
- matrix: number[][];
120
+ export class Example extends via((field) => ({
121
+ // Array of strings
122
+ tags: field([String]),
167
123
 
168
- // 3D coordinate array
169
- @Field.Prop(() => [[[Float]]])
170
- coordinates: number[][][];
171
- ```
124
+ // Array of numbers
125
+ scores: field([Int]),
172
126
 
173
- ### Array of Scalars
127
+ // Array of other scalars
128
+ items: field([OtherScalar]),
174
129
 
175
- ```typescript
176
- @Field.Prop(() => [OtherScalar])
177
- items: OtherScalar[];
130
+ // Nested arrays (2D matrix)
131
+ matrix: field([[Int]]),
132
+ })) {}
178
133
  ```
179
134
 
180
- ## Map Fields
135
+ ### Optional Fields
181
136
 
182
- ```typescript
183
- @Field.Prop(() => Map, {
184
- of: String, // Must specify value type
185
- default: new Map()
186
- })
187
- metadata: Map<string, string>;
188
- ```
189
-
190
- ## Enum Implementation (camelCase Values)
137
+ Optional fields can be defined using the `.optional()` chain:
191
138
 
192
139
  ```typescript
193
- // Define enum with camelCase values (export REQUIRED)
194
- export const Status = enumOf(["active", "inactive", "pendingApproval"] as const);
195
- export type Status = enumOf<typeof Status>; // Type export
196
-
197
- // Use in field
198
- @Field.Prop(() => String, {
199
- enum: Status,
200
- default: "active"
201
- })
202
- status: Status;
140
+ import { ID, Int } from "@akanjs/base";
141
+ import { via } from "@akanjs/constant";
142
+
143
+ export class FileMeta extends via((field) => ({
144
+ fileId: field(ID).optional(), // Optional field
145
+ lastModifiedAt: field(Date),
146
+ size: field(Int),
147
+ })) {}
203
148
  ```
204
149
 
205
- ## Static Methods
150
+ ## Field Options Reference
206
151
 
207
- Scalar classes can include static methods for common operations on the scalar type:
152
+ | Option | Type | Default | Description | Example |
153
+ | ----------- | -------- | ----------- | ------------------------------------------------ | ----------------------------- |
154
+ | `default` | Any/Func | `undefined` | Default field value (static or factory function) | `{ default: 0 }` |
155
+ | `min` | Number | - | Minimum numeric value | `{ min: 0 }` |
156
+ | `max` | Number | - | Maximum numeric value | `{ max: 100 }` |
157
+ | `minlength` | Number | - | Minimum string length | `{ minlength: 3 }` |
158
+ | `maxlength` | Number | - | Maximum string length | `{ maxlength: 255 }` |
159
+ | `validate` | Function | - | Custom validation function | `{ validate: isPhoneNumber }` |
160
+ | `example` | Any | - | Example value for documentation | `{ example: [0, 0] }` |
161
+
162
+ ### Field Options Examples
208
163
 
209
164
  ```typescript
210
- export class Coordinate {
211
- @Field.Prop(() => [Float], { default: [0, 0] })
212
- coordinates: number[];
165
+ import { dayjs, Int, Float } from "@akanjs/base";
166
+ import { isPhoneNumber } from "@akanjs/common";
167
+ import { via } from "@akanjs/constant";
213
168
 
214
- // Static utility methods
215
- static getDistanceKm(loc1: Coordinate, loc2: Coordinate) {
216
- const [lon1, lat1] = loc1.coordinates;
217
- const [lon2, lat2] = loc2.coordinates;
218
- // Distance calculation logic
219
- return distance;
220
- }
221
- }
222
- ```
169
+ export class OrderInfo extends via((field) => ({
170
+ // Static default value
171
+ quantity: field(Int, { default: 1 }),
223
172
 
224
- ## Validation Rules and Checklist
173
+ // Dynamic default using factory function
174
+ orderedAt: field(Date, { default: () => dayjs() }),
225
175
 
226
- ### File-Level Validation
176
+ // Number range validation
177
+ rating: field(Float, { min: 0, max: 5 }),
227
178
 
228
- - [ ] Location: `__scalar/<camelCase>/<camelCase>.constant.ts`
229
- - [ ] Single `@Model.Scalar` class per file
230
- - [ ] Class name matches directory name (PascalCase)
231
- - [ ] All required imports present
232
- - [ ] The string passed to `@Model.Scalar()` must match the class name exactly
179
+ // String length validation
180
+ description: field(String, { minlength: 10, maxlength: 500 }),
233
181
 
234
- ### Field-Level Validation
182
+ // Custom validation function
183
+ phone: field(String, { validate: isPhoneNumber }),
235
184
 
236
- - [ ] All fields have `@Field.Prop` decorator (or `@Field.Hidden`, `@Field.Secret`, `@Field.Resolve`)
237
- - [ ] Type references use arrow functions: `() => Type`
238
- - [ ] Array types use bracket notation: `[Type]`
239
- - [ ] Nullable fields use TypeScript union: `Type | null`
240
- - [ ] Default values match field types
241
- - [ ] Enums are properly defined with camelCase values and exported
242
- - [ ] Custom validation functions are pure and side-effect free
243
- - [ ] Dayjs type is used for date fields
185
+ // Combined options
186
+ price: field(Float, {
187
+ default: 0,
188
+ min: 0,
189
+ example: 29.99,
190
+ }),
191
+ })) {}
192
+ ```
244
193
 
245
- ### Best Practices
194
+ ### Static vs Dynamic Defaults
246
195
 
247
- - **Focused models**: Each scalar should represent a single concept
248
- - **Descriptive names**: Use clear, domain-relevant terminology
249
- - **Enum Values**: Always use camelCase (`active`, not `ACTIVE`)
250
- - **Reusability**: Create separate scalars for commonly used structures
251
- - **Documentation**: Add comments for complex fields
252
- - **Validation**: Use min/max for numbers, minlength/maxlength for text
253
- - **Immutability**: Mark fields as `immutable` where appropriate
254
- - **Default values**: Provide sensible defaults for most fields
196
+ - Use **static values** for constants: `{ default: 0 }`, `{ default: "active" }`
197
+ - Use **factory functions** for values computed at creation time: `{ default: () => dayjs() }`, `{ default: () => new ObjectId() }`
255
198
 
256
- ## Common Mistakes and Fixes
199
+ ## Enum Definition with enumOf()
200
+
201
+ The `enumOf()` function creates typed enum classes. Define enums before using them in your scalar fields.
257
202
 
258
- ### Incorrect Enum Case
203
+ ### Basic Enum
259
204
 
260
205
  ```typescript
261
- // Wrong (uppercase values)
262
- export const Status = enumOf(["ACTIVE", "INACTIVE"] as const);
206
+ import { enumOf } from "@akanjs/base";
207
+ import { via } from "@akanjs/constant";
208
+
209
+ // Simple enum definition
210
+ export class NotiSetting extends enumOf("notiSetting", ["disagree", "fewer", "normal", "block"]) {}
263
211
 
264
- // Correct (camelCase values)
265
- export const Status = enumOf(["active", "inactive"] as const);
212
+ // Using enum in a scalar
213
+ export class NotiInfo extends via((field) => ({
214
+ setting: field(NotiSetting, { default: "normal" }),
215
+ })) {}
266
216
  ```
267
217
 
268
- ### Incorrect Array Syntax
218
+ ### Enum with 'as const'
219
+
220
+ For better TypeScript type inference, use `as const`:
269
221
 
270
222
  ```typescript
271
- // Wrong
272
- @Field.Prop(() => Array<Int>)
273
- values: number[];
223
+ import { dayjs, enumOf } from "@akanjs/base";
224
+ import { via } from "@akanjs/constant";
225
+
226
+ // Use "as const" for better type inference
227
+ export class Status extends enumOf("status", ["pending", "active", "completed", "cancelled"] as const) {}
274
228
 
275
- // Correct
276
- @Field.Prop(() => [Int])
277
- values: number[];
229
+ export class Order extends via((field) => ({
230
+ status: field(Status, { default: "pending" }),
231
+ orderedAt: field(Date, { default: () => dayjs() }),
232
+ })) {}
278
233
  ```
279
234
 
280
- ### Missing Nullable Type Declaration
235
+ ### Enum Key Points
281
236
 
282
- ```typescript
283
- // Wrong (type mismatch)
284
- @Field.Prop(() => String, { nullable: true })
285
- description: string;
237
+ | Point | Description |
238
+ | ---------------------- | ---------------------------------------------------------------------------------------- |
239
+ | `enumOf(name, values)` | First argument is the enum name (used in dictionary/GraphQL). Second is the value array. |
240
+ | camelCase Values | Always use camelCase for enum values (e.g., `"waitPay"`, not `"WAIT_PAY"`). |
241
+ | `as const` | Add `as const` to the values array for better TypeScript type inference. |
286
242
 
287
- // Correct
288
- @Field.Prop(() => String, { nullable: true })
289
- description: string | null;
290
- ```
243
+ ## Instance Methods
291
244
 
292
- ### Improper Enum Implementation
245
+ You can add instance methods directly to the scalar class for computed properties and utility functions:
293
246
 
294
247
  ```typescript
295
- // Wrong (missing export/type)
296
- const Status = enumOf(["active"] as const);
297
-
298
- // Correct
299
- export const Status = enumOf(["active"] as const);
300
- export type Status = enumOf<typeof Status>;
248
+ import { Int } from "@akanjs/base";
249
+ import { via } from "@akanjs/constant";
250
+
251
+ export class Stock extends via((field) => ({
252
+ total: field(Int, { default: 0, min: 0 }),
253
+ current: field(Int, { default: 0, min: 0 }),
254
+ })) {
255
+ getPercentage() {
256
+ if (this.total === 0) return 0;
257
+ return (this.current / this.total) * 100;
258
+ }
259
+ }
301
260
  ```
302
261
 
303
- ### Multiple Models in One File
262
+ ### Method Guidelines
263
+
264
+ - Instance methods have access to all fields via `this`
265
+ - Use them for calculations based on field values
266
+ - Methods defined in `constant.ts` are available on both server and client
267
+ - For server-only logic, use `document.ts` instead
268
+
269
+ ## Static Methods
270
+
271
+ Scalar classes can include static methods for common operations:
304
272
 
305
273
  ```typescript
306
- // Wrong (violates single-responsibility)
307
- class Address { ... }
308
- class User { ... }
274
+ import { enumOf, Float } from "@akanjs/base";
275
+ import { via } from "@akanjs/constant";
309
276
 
310
- // Correct (separate files)
311
- // address.constant.ts
312
- export class Address { ... }
277
+ export class CoordinateType extends enumOf("coordinateType", ["Point"] as const) {}
278
+
279
+ export class Coordinate extends via((field) => ({
280
+ type: field(CoordinateType, { default: "Point" }),
281
+ coordinates: field([Float], { default: [0, 0], example: [127.114367, 37.497114] }),
282
+ altitude: field(Float, { default: 0 }),
283
+ })) {
284
+ static getDistanceKm(loc1: Coordinate, loc2: Coordinate) {
285
+ const [lon1, lat1] = loc1.coordinates;
286
+ const [lon2, lat2] = loc2.coordinates;
287
+ const R = 6371; // Earth's radius in kilometers
288
+ // Distance calculation logic...
289
+ return distance;
290
+ }
313
291
 
314
- // user.constant.ts
315
- import { Address } from "../address/address.constant";
316
- export class User { ... }
292
+ static moveMeters(loc: Coordinate, x: number, y: number): Coordinate {
293
+ // Calculate new position...
294
+ return { ...loc, coordinates: [newLon, newLat] };
295
+ }
296
+ }
317
297
  ```
318
298
 
319
- ### Incorrect Date Handling
299
+ ## Common Mistakes and Fixes
320
300
 
321
- ```typescript
322
- // Wrong (uses native Date)
323
- @Field.Prop(() => Date)
324
- createdAt: Date;
301
+ | Issue | Wrong ❌ | Correct ✅ |
302
+ | --------------- | ---------------------------------- | ----------------------------------------- |
303
+ | Enum case | `enumOf("status", ["ACTIVE"])` | `enumOf("status", ["active"])` |
304
+ | Array syntax | `field(Array<Int>)` | `field([Int])` |
305
+ | Dynamic default | `{ default: dayjs() }` | `{ default: () => dayjs() }` |
306
+ | Missing export | `class Status extends enumOf(...)` | `export class Status extends enumOf(...)` |
307
+ | Optional field | `field(ID, { nullable: true })` | `field(ID).optional()` |
308
+
309
+ ### Important Note
325
310
 
326
- // Correct (uses Dayjs)
327
- import { type Dayjs } from "@akanjs/base";
311
+ For Date defaults that should be computed at creation time, **always use a factory function**:
328
312
 
329
- @Field.Prop(() => Date)
330
- createdAt: Dayjs;
313
+ ```typescript
314
+ // ❌ Wrong - creates a single fixed date at module load
315
+ { default: dayjs() }
316
+
317
+ // ✅ Correct - creates a new date each time
318
+ { default: () => dayjs() }
331
319
  ```
332
320
 
333
- ### Missing Model.Scalar Parameter
321
+ ## Implementation Checklist
334
322
 
335
- ```typescript
336
- // ❌ Wrong (missing or mismatched parameter)
337
- @Model.Scalar()
338
- export class GeoLocation { ... }
323
+ ### File-Level
339
324
 
340
- // Correct
325
+ - [ ] File location: `__scalar/<camelCase>/<camelCase>.constant.ts`
326
+ - [ ] Import `via` from `@akanjs/constant`
327
+ - [ ] Import `enumOf` from `@akanjs/base` (if using enums)
328
+ - [ ] Export all classes (scalar and enums)
341
329
 
342
- export class GeoLocation { ... }
343
- ```
330
+ ### Naming
331
+
332
+ - [ ] Use PascalCase for class names
333
+ - [ ] Use camelCase for enum values
334
+ - [ ] Class name matches directory name in PascalCase
335
+
336
+ ### Field Definition
337
+
338
+ - [ ] Use `[Type]` syntax for arrays
339
+ - [ ] Use factory functions for dynamic defaults
340
+ - [ ] Use `.optional()` for nullable fields
341
+ - [ ] Add `as const` for large enum value arrays
344
342
 
345
343
  ## Full Examples
346
344
 
@@ -349,136 +347,96 @@ export class GeoLocation { ... }
349
347
  ```typescript
350
348
  // libs/payment/lib/__scalar/amount/amount.constant.ts
351
349
  import { Float } from "@akanjs/base";
352
- import { Field, Model } from "@akanjs/constant";
353
-
354
- export class Amount {
355
- @Field.Prop(() => Float, { min: 0, default: 0 })
356
- value: number;
350
+ import { via } from "@akanjs/constant";
357
351
 
358
- @Field.Prop(() => String, { default: "USD" })
359
- currency: string;
360
- }
352
+ export class Amount extends via((field) => ({
353
+ value: field(Float, { min: 0, default: 0 }),
354
+ currency: field(String, { default: "USD" }),
355
+ })) {}
361
356
  ```
362
357
 
363
- ### Complex Scalar Example (with camelCase enums)
358
+ ### Scalar with Enum
364
359
 
365
360
  ```typescript
366
- // apps/maps/lib/__scalar/geoLocation/geoLocation.constant.ts
367
- import { Float, type Dayjs, dayjs, enumOf } from "@akanjs/base";
368
- import { Field, Model } from "@akanjs/constant";
369
-
370
- // CORRECT: camelCase enum values
371
- export const AccuracyLevel = enumOf(["low", "medium", "high"] as const);
372
- export type AccuracyLevel = enumOf<typeof AccuracyLevel>;
373
-
374
- export class GeoLocation {
375
- @Field.Prop(() => Float, {
376
- min: -90,
377
- max: 90,
378
- example: 37.7749,
379
- })
380
- latitude: number;
381
-
382
- @Field.Prop(() => Float, {
383
- min: -180,
384
- max: 180,
385
- example: -122.4194,
386
- })
387
- longitude: number;
388
-
389
- @Field.Prop(() => Float, {
390
- nullable: true,
391
- min: 0,
392
- example: 12.5,
393
- })
394
- elevation: number | null;
395
-
396
- @Field.Prop(() => String, {
397
- enum: AccuracyLevel,
398
- default: "medium",
399
- })
400
- accuracy: AccuracyLevel;
401
-
402
- @Field.Prop(() => Date, {
403
- default: () => dayjs(),
404
- immutable: true,
405
- })
406
- measuredAt: Dayjs;
407
- }
361
+ // apps/akasys/lib/__scalar/version/version.constant.ts
362
+ import { dayjs, enumOf } from "@akanjs/base";
363
+ import { via } from "@akanjs/constant";
364
+
365
+ export class VersionStatus extends enumOf("versionStatus", ["active", "expired"] as const) {}
366
+
367
+ export class Version extends via((field) => ({
368
+ source: field(File).optional(),
369
+ appBuild: field(File).optional(),
370
+ build: field(File).optional(),
371
+ status: field(VersionStatus, { default: "active" }),
372
+ at: field(Date, { default: () => dayjs() }),
373
+ })) {}
408
374
  ```
409
375
 
410
- ### Scalar with Nested Objects
376
+ ### Scalar with Optional Fields
411
377
 
412
378
  ```typescript
413
- // apps/ecommerce/lib/__scalar/product-spec/product-spec.constant.ts
414
- import { Int, Float } from "@akanjs/base";
415
- import { Field, Model } from "@akanjs/constant";
416
- import { Dimension } from "../dimension/dimension.constant";
417
-
418
- export class ProductSpec {
419
- @Field.Prop(() => String)
420
- sku: string;
421
-
422
- @Field.Prop(() => [String], {
423
- default: [],
424
- example: ["red", "blue"],
425
- })
426
- colors: string[];
427
-
428
- @Field.Prop(() => Dimension)
429
- size: Dimension;
430
-
431
- @Field.Prop(() => Float, { min: 0 })
432
- weight: number;
433
-
434
- @Field.Prop(() => Int, {
435
- min: 0,
436
- default: 0,
437
- })
438
- stock: number;
439
- }
379
+ // apps/angelo/lib/__scalar/estimate/estimate.constant.ts
380
+ import { Float, Int } from "@akanjs/base";
381
+ import { via } from "@akanjs/constant";
382
+
383
+ export class Estimate extends via((field) => ({
384
+ name: field(String),
385
+ value: field(Float, { min: 0, default: 0 }),
386
+ num: field(Int, { min: 1, default: 1 }),
387
+ unit: field(String).optional(),
388
+ note: field(String).optional(),
389
+ })) {}
440
390
  ```
441
391
 
442
- ### Scalar with Static Methods
392
+ ### Complex Scalar with Static Methods
443
393
 
444
394
  ```typescript
445
395
  // libs/util/lib/__scalar/coordinate/coordinate.constant.ts
446
- import { Float } from "@akanjs/base";
447
- import { Field, Model } from "@akanjs/constant";
396
+ import { enumOf, Float } from "@akanjs/base";
397
+ import { via } from "@akanjs/constant";
448
398
 
449
- export class Coordinate {
450
- @Field.Prop(() => [Float], { default: [0, 0], example: [127.114367, 37.497114] })
451
- coordinates: number[];
452
-
453
- @Field.Prop(() => Float, { default: 0 })
454
- altitude: number;
399
+ export class CoordinateType extends enumOf("coordinateType", ["Point"] as const) {}
455
400
 
401
+ export class Coordinate extends via((field) => ({
402
+ type: field(CoordinateType, { default: "Point" }),
403
+ coordinates: field([Float], { default: [0, 0], example: [127.114367, 37.497114] }),
404
+ altitude: field(Float, { default: 0 }),
405
+ })) {
456
406
  static getDistanceKm(loc1: Coordinate, loc2: Coordinate) {
457
407
  const [lon1, lat1] = loc1.coordinates;
458
408
  const [lon2, lat2] = loc2.coordinates;
459
- // Distance calculation logic...
409
+ const R = 6371;
410
+ // ... calculation logic
460
411
  return distance;
461
412
  }
462
413
 
463
- static moveMeters(loc: Coordinate, x: number, y: number): Coordinate {
464
- // Calculate new position...
465
- return { ...loc, coordinates: [newLon, newLat] };
414
+ static moveMeters(loc: Coordinate, x: number, y: number, z: number = 0): Coordinate {
415
+ const [lon, lat] = loc.coordinates;
416
+ const dx = ((x / 1000 / 6371) * (180 / Math.PI)) / Math.cos(lat * (Math.PI / 180));
417
+ const dy = (y / 1000 / 6371) * (180 / Math.PI);
418
+ return { ...loc, coordinates: [lon + dx, lat + dy], altitude: loc.altitude + z };
466
419
  }
467
420
  }
468
421
  ```
469
422
 
470
- ## Summary Checklist
423
+ ## Pro Tips
424
+
425
+ - **Keep scalars focused**: If it grows too large, split it into multiple scalars
426
+ - **Value objects only**: Avoid adding ID or timestamp fields (use Models for that)
427
+ - **Define enums first**: Define enums before the scalar class that uses them
428
+ - **Dictionary support**: Don't forget to create `dictionary.ts` for i18n support
429
+ - **Reusability**: Create separate scalars for commonly used structures
430
+
431
+ ## Summary
471
432
 
472
433
  1. **Location**: `__scalar/<camelCase>/<camelCase>.constant.ts`
473
- 2. **Structure**: Single `@Model.Scalar` class per file
474
- 3. **Naming**: Class name = PascalCase directory name
475
- 4. **Decorator**: `` with exact class name match
476
- 5. **Enum Values**: Always camelCase (`active`, not `ACTIVE`)
477
- 6. **Fields**: Use `@Field.Prop` with arrow function types
478
- 7. **Arrays**: Wrap in `[]` (e.g., `[Int]`)
479
- 8. **Enums**: Export with `enumOf` and derived type
480
- 9. **Dates**: Always use `Dayjs` type with `Date` decorator
481
- 10. **Nullability**: Use `| null` with `{ nullable: true }`
482
- 11. **Validation**: Implement field-level constraints
434
+ 2. **Import**: `via` from `@akanjs/constant`, `enumOf` from `@akanjs/base`
435
+ 3. **Scalar Class**: `export class Name extends via((field) => ({...})) {}`
436
+ 4. **Enum Class**: `export class EnumName extends enumOf("enumName", [...] as const) {}`
437
+ 5. **Fields**: `field(Type)` or `field(Type, { options })`
438
+ 6. **Optional**: Use `.optional()` chain method
439
+ 7. **Arrays**: Use `[Type]` syntax
440
+ 8. **Enum Values**: Always camelCase
483
441
 
484
442
  Following these patterns ensures type-safe, maintainable scalar definitions that integrate seamlessly with the Akan.js framework's data modeling layer.