@shetty4l/core 0.1.37 → 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
|
@@ -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
|
|
97
|
+
return { sql: `${column} = ?`, params: [serializeBindValue(value)] };
|
|
79
98
|
|
|
80
99
|
case "neq":
|
|
81
|
-
return { sql: `${column} != ?`, params: [value
|
|
100
|
+
return { sql: `${column} != ?`, params: [serializeBindValue(value)] };
|
|
82
101
|
|
|
83
102
|
case "lt":
|
|
84
|
-
return { sql: `${column} < ?`, params: [value
|
|
103
|
+
return { sql: `${column} < ?`, params: [serializeBindValue(value)] };
|
|
85
104
|
|
|
86
105
|
case "lte":
|
|
87
|
-
return { sql: `${column} <= ?`, params: [value
|
|
106
|
+
return { sql: `${column} <= ?`, params: [serializeBindValue(value)] };
|
|
88
107
|
|
|
89
108
|
case "gt":
|
|
90
|
-
return { sql: `${column} > ?`, params: [value
|
|
109
|
+
return { sql: `${column} > ?`, params: [serializeBindValue(value)] };
|
|
91
110
|
|
|
92
111
|
case "gte":
|
|
93
|
-
return { sql: `${column} >= ?`, params: [value
|
|
112
|
+
return { sql: `${column} >= ?`, params: [serializeBindValue(value)] };
|
|
94
113
|
|
|
95
114
|
case "in": {
|
|
96
|
-
const arr = value as
|
|
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 {
|
|
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
|
|
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 {
|
|
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.
|
package/src/state/loader.ts
CHANGED
|
@@ -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
|
/**
|