@shetty4l/core 0.1.36 → 0.1.38

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shetty4l/core",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "Shared infrastructure primitives for Bun/TypeScript services",
5
5
  "repository": {
6
6
  "type": "git",
@@ -192,10 +192,21 @@ export function Id(type: FieldType = "string", options?: IdOptions) {
192
192
  *
193
193
  * @param type - The field type ('string' | 'number' | 'boolean' | 'date')
194
194
  * @param options - Optional field configuration (column name override)
195
+ * @throws Error if property name is a reserved timestamp field (created_at, updated_at)
195
196
  */
196
197
  export function Field(type: FieldType, options?: FieldOptions) {
197
198
  return function (_target: undefined, context: unknown): void {
198
199
  const property = extractPropertyName(context);
200
+
201
+ // Reject reserved timestamp field names
202
+ if (property === "created_at" || property === "updated_at") {
203
+ resetGlobalState();
204
+ throw new Error(
205
+ `"${property}" is reserved for auto-managed timestamps. ` +
206
+ `Remove the @Field decorator - timestamps are automatically populated by StateLoader.`,
207
+ );
208
+ }
209
+
199
210
  const column = options?.column ?? toSnakeCase(property);
200
211
 
201
212
  // Accumulate field definitions
@@ -50,16 +50,35 @@ function getColumn(meta: CollectionMeta, property: string): string {
50
50
  return meta.idColumn;
51
51
  }
52
52
 
53
+ // Handle auto-managed timestamp fields
54
+ if (property === "created_at") {
55
+ return "created_at";
56
+ }
57
+ if (property === "updated_at") {
58
+ return "updated_at";
59
+ }
60
+
53
61
  const field = meta.fields.get(property);
54
62
  if (!field) {
55
63
  throw new Error(
56
64
  `Property "${property}" not found in collection "${meta.table}". ` +
57
- `Available fields: ${meta.idProperty}, ${[...meta.fields.keys()].join(", ")}`,
65
+ `Available fields: ${meta.idProperty}, ${[...meta.fields.keys()].join(", ")}, created_at, updated_at`,
58
66
  );
59
67
  }
60
68
  return field.column;
61
69
  }
62
70
 
71
+ /**
72
+ * Serialize a value for SQL binding.
73
+ * Converts Date objects to ISO strings, passes other values through.
74
+ */
75
+ function serializeBindValue(value: unknown): SQLQueryBindings {
76
+ if (value instanceof Date) {
77
+ return value.toISOString();
78
+ }
79
+ return value as SQLQueryBindings;
80
+ }
81
+
63
82
  /**
64
83
  * Build SQL condition fragment and params for a single operator.
65
84
  *
@@ -75,41 +94,47 @@ function buildOperatorCondition(
75
94
  ): { sql: string; params: SQLQueryBindings[] } {
76
95
  switch (op) {
77
96
  case "eq":
78
- return { sql: `${column} = ?`, params: [value as SQLQueryBindings] };
97
+ return { sql: `${column} = ?`, params: [serializeBindValue(value)] };
79
98
 
80
99
  case "neq":
81
- return { sql: `${column} != ?`, params: [value as SQLQueryBindings] };
100
+ return { sql: `${column} != ?`, params: [serializeBindValue(value)] };
82
101
 
83
102
  case "lt":
84
- return { sql: `${column} < ?`, params: [value as SQLQueryBindings] };
103
+ return { sql: `${column} < ?`, params: [serializeBindValue(value)] };
85
104
 
86
105
  case "lte":
87
- return { sql: `${column} <= ?`, params: [value as SQLQueryBindings] };
106
+ return { sql: `${column} <= ?`, params: [serializeBindValue(value)] };
88
107
 
89
108
  case "gt":
90
- return { sql: `${column} > ?`, params: [value as SQLQueryBindings] };
109
+ return { sql: `${column} > ?`, params: [serializeBindValue(value)] };
91
110
 
92
111
  case "gte":
93
- return { sql: `${column} >= ?`, params: [value as SQLQueryBindings] };
112
+ return { sql: `${column} >= ?`, params: [serializeBindValue(value)] };
94
113
 
95
114
  case "in": {
96
- const arr = value as SQLQueryBindings[];
115
+ const arr = value as unknown[];
97
116
  if (!Array.isArray(arr) || arr.length === 0) {
98
117
  // Empty IN clause: always false
99
118
  return { sql: "0 = 1", params: [] };
100
119
  }
101
120
  const placeholders = arr.map(() => "?").join(", ");
102
- return { sql: `${column} IN (${placeholders})`, params: arr };
121
+ return {
122
+ sql: `${column} IN (${placeholders})`,
123
+ params: arr.map(serializeBindValue),
124
+ };
103
125
  }
104
126
 
105
127
  case "notIn": {
106
- const arr = value as SQLQueryBindings[];
128
+ const arr = value as unknown[];
107
129
  if (!Array.isArray(arr) || arr.length === 0) {
108
130
  // Empty NOT IN clause: always true (no exclusions)
109
131
  return { sql: "1 = 1", params: [] };
110
132
  }
111
133
  const placeholders = arr.map(() => "?").join(", ");
112
- return { sql: `${column} NOT IN (${placeholders})`, params: arr };
134
+ return {
135
+ sql: `${column} NOT IN (${placeholders})`,
136
+ params: arr.map(serializeBindValue),
137
+ };
113
138
  }
114
139
 
115
140
  case "isNull":
@@ -128,6 +128,10 @@ export interface FindOptions<T> {
128
128
  * from singleton @Persisted classes. Subclasses must be decorated with
129
129
  * @PersistedCollection.
130
130
  *
131
+ * Auto-managed timestamps (`created_at`, `updated_at`) are populated when
132
+ * entities are loaded from the database. These are read-only and cannot be
133
+ * set via @Field decorators.
134
+ *
131
135
  * @example
132
136
  * ```ts
133
137
  * @PersistedCollection('users')
@@ -143,9 +147,34 @@ export interface FindOptions<T> {
143
147
  * // Implemented by StateLoader binding
144
148
  * }
145
149
  * }
150
+ *
151
+ * // Usage: timestamps are available after load
152
+ * const user = loader.get(User, 'abc123');
153
+ * console.log(user.created_at); // Date when entity was created
154
+ * console.log(user.updated_at); // Date when entity was last modified
155
+ *
156
+ * // Query by timestamps
157
+ * const recent = loader.find(User, {
158
+ * where: { updated_at: { op: 'gte', value: yesterday } },
159
+ * orderBy: { created_at: 'desc' }
160
+ * });
146
161
  * ```
147
162
  */
148
163
  export abstract class CollectionEntity {
164
+ /**
165
+ * Timestamp when this entity was first created.
166
+ * Auto-managed by StateLoader; populated on load.
167
+ * Default value before population is epoch (1970-01-01).
168
+ */
169
+ readonly created_at: Date = new Date(0);
170
+
171
+ /**
172
+ * Timestamp when this entity was last modified.
173
+ * Auto-managed by StateLoader; updated on every save().
174
+ * Default value before population is epoch (1970-01-01).
175
+ */
176
+ readonly updated_at: Date = new Date(0);
177
+
149
178
  /**
150
179
  * Persist this entity to the database.
151
180
  * Implemented when the entity is bound to a StateLoader.
@@ -65,8 +65,14 @@ export { Field, Persisted } from "./decorators";
65
65
 
66
66
  /** Mark a class as a persisted collection (multi-row table). */
67
67
  /** Mark a property as the primary key field. */
68
+ /** Mark a property as a persisted field in a collection (use with @PersistedCollection). */
68
69
  /** Mark column(s) for indexing. */
69
- export { Id, Index, PersistedCollection } from "./collection/decorators";
70
+ export {
71
+ Field as CollectionField,
72
+ Id,
73
+ Index,
74
+ PersistedCollection,
75
+ } from "./collection/decorators";
70
76
 
71
77
  // --------------------------------------------------------------------------
72
78
  // Collection base class
@@ -753,6 +753,19 @@ export class StateLoader {
753
753
  (instance as Record<string, unknown>)[field.property] = value;
754
754
  }
755
755
  }
756
+
757
+ // Set auto-managed timestamps
758
+ const rawCreatedAt = row.created_at;
759
+ if (rawCreatedAt !== null && rawCreatedAt !== undefined) {
760
+ const createdAtValue = deserializeValue(rawCreatedAt, "date");
761
+ (instance as Record<string, unknown>).created_at = createdAtValue;
762
+ }
763
+
764
+ const rawUpdatedAt = row.updated_at;
765
+ if (rawUpdatedAt !== null && rawUpdatedAt !== undefined) {
766
+ const updatedAtValue = deserializeValue(rawUpdatedAt, "date");
767
+ (instance as Record<string, unknown>).updated_at = updatedAtValue;
768
+ }
756
769
  }
757
770
 
758
771
  /**