@airoom/nextmin-node 0.1.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.
- package/LICENSE +49 -0
- package/README.md +178 -0
- package/dist/api/apiRouter.d.ts +65 -0
- package/dist/api/apiRouter.js +1548 -0
- package/dist/database/DatabaseAdapter.d.ts +14 -0
- package/dist/database/DatabaseAdapter.js +2 -0
- package/dist/database/InMemoryAdapter.d.ts +15 -0
- package/dist/database/InMemoryAdapter.js +71 -0
- package/dist/database/MongoAdapter.d.ts +52 -0
- package/dist/database/MongoAdapter.js +409 -0
- package/dist/files/FileStorageAdapter.d.ts +35 -0
- package/dist/files/FileStorageAdapter.js +2 -0
- package/dist/files/S3FileStorageAdapter.d.ts +30 -0
- package/dist/files/S3FileStorageAdapter.js +84 -0
- package/dist/files/filename.d.ts +5 -0
- package/dist/files/filename.js +40 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +87 -0
- package/dist/models/BaseModel.d.ts +44 -0
- package/dist/models/BaseModel.js +31 -0
- package/dist/policy/authorize.d.ts +25 -0
- package/dist/policy/authorize.js +305 -0
- package/dist/policy/conditions.d.ts +14 -0
- package/dist/policy/conditions.js +30 -0
- package/dist/policy/types.d.ts +53 -0
- package/dist/policy/types.js +2 -0
- package/dist/policy/utils.d.ts +9 -0
- package/dist/policy/utils.js +118 -0
- package/dist/schemas/Roles.json +64 -0
- package/dist/schemas/Settings.json +62 -0
- package/dist/schemas/Users.json +123 -0
- package/dist/services/SchemaService.d.ts +10 -0
- package/dist/services/SchemaService.js +46 -0
- package/dist/utils/DefaultDataInitializer.d.ts +21 -0
- package/dist/utils/DefaultDataInitializer.js +269 -0
- package/dist/utils/Logger.d.ts +12 -0
- package/dist/utils/Logger.js +79 -0
- package/dist/utils/SchemaLoader.d.ts +51 -0
- package/dist/utils/SchemaLoader.js +323 -0
- package/dist/utils/apiKey.d.ts +5 -0
- package/dist/utils/apiKey.js +14 -0
- package/dist/utils/fieldCodecs.d.ts +13 -0
- package/dist/utils/fieldCodecs.js +133 -0
- package/package.json +45 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SchemaLoader = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const chokidar_1 = __importDefault(require("chokidar"));
|
|
10
|
+
const Logger_1 = __importDefault(require("./Logger"));
|
|
11
|
+
const events_1 = require("events");
|
|
12
|
+
function validateSchema(obj) {
|
|
13
|
+
const missing = [];
|
|
14
|
+
if (!obj.modelName || typeof obj.modelName !== 'string')
|
|
15
|
+
missing.push('modelName');
|
|
16
|
+
if (!obj.attributes ||
|
|
17
|
+
typeof obj.attributes !== 'object' ||
|
|
18
|
+
Array.isArray(obj.attributes)) {
|
|
19
|
+
missing.push('attributes');
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
for (const [key, attr] of Object.entries(obj.attributes)) {
|
|
23
|
+
if (Array.isArray(attr)) {
|
|
24
|
+
// Each element in the array must be an object with type:string
|
|
25
|
+
if (attr.length === 0 ||
|
|
26
|
+
attr.some((item) => item === null ||
|
|
27
|
+
typeof item !== 'object' ||
|
|
28
|
+
Array.isArray(item) ||
|
|
29
|
+
typeof item.type !== 'string')) {
|
|
30
|
+
missing.push(`attributes.${key}[]`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (attr === null ||
|
|
35
|
+
typeof attr !== 'object' ||
|
|
36
|
+
Array.isArray(attr) ||
|
|
37
|
+
// @ts-ignore
|
|
38
|
+
typeof attr.type !== 'string') {
|
|
39
|
+
missing.push(`attributes.${key}.type`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!obj.allowedMethods ||
|
|
45
|
+
typeof obj.allowedMethods !== 'object' ||
|
|
46
|
+
Array.isArray(obj.allowedMethods)) {
|
|
47
|
+
missing.push('allowedMethods');
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const allowed = obj.allowedMethods;
|
|
51
|
+
if (typeof allowed.create !== 'boolean' &&
|
|
52
|
+
typeof allowed.read !== 'boolean' &&
|
|
53
|
+
typeof allowed.update !== 'boolean' &&
|
|
54
|
+
typeof allowed.delete !== 'boolean') {
|
|
55
|
+
missing.push('allowedMethods.(at least one CRUD boolean)');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return missing;
|
|
59
|
+
}
|
|
60
|
+
class SchemaLoader {
|
|
61
|
+
// Singleton instance
|
|
62
|
+
static getInstance() {
|
|
63
|
+
if (!SchemaLoader._instance) {
|
|
64
|
+
SchemaLoader._instance = new SchemaLoader();
|
|
65
|
+
}
|
|
66
|
+
return SchemaLoader._instance;
|
|
67
|
+
}
|
|
68
|
+
constructor() {
|
|
69
|
+
this.emitter = new events_1.EventEmitter();
|
|
70
|
+
this.schemas = {};
|
|
71
|
+
this.isDevelopment = false;
|
|
72
|
+
this.nonOverridableSchemas = new Set(['User', 'Role']);
|
|
73
|
+
this.watcher = null; // <-- TS-safe
|
|
74
|
+
this.isDevelopment = process.env.APP_MODE !== 'production';
|
|
75
|
+
const baseDir = process.cwd();
|
|
76
|
+
const basePackageDir = path_1.default.resolve(__dirname, '..');
|
|
77
|
+
this.packageSchemasDir = path_1.default.join(basePackageDir, 'schemas');
|
|
78
|
+
this.userSchemasDir = path_1.default.resolve(baseDir, 'schemas');
|
|
79
|
+
this.loadSchemas();
|
|
80
|
+
if (this.isDevelopment) {
|
|
81
|
+
this.setupHotReload();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
setupHotReload() {
|
|
85
|
+
if (!fs_1.default.existsSync(this.userSchemasDir)) {
|
|
86
|
+
Logger_1.default.warn(`User schemas directory not found for hot reload: ${this.userSchemasDir}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
Logger_1.default.info(`Watching user schemas directory for hot reload: ${this.userSchemasDir}`);
|
|
90
|
+
this.watcher = chokidar_1.default.watch(this.userSchemasDir, {
|
|
91
|
+
persistent: true,
|
|
92
|
+
ignoreInitial: true,
|
|
93
|
+
depth: 1,
|
|
94
|
+
awaitWriteFinish: {
|
|
95
|
+
stabilityThreshold: 200,
|
|
96
|
+
pollInterval: 100,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
this.watcher
|
|
100
|
+
.on('add', (filePath) => this.handleFileChange('added', filePath))
|
|
101
|
+
.on('change', (filePath) => this.handleFileChange('changed', filePath))
|
|
102
|
+
.on('unlink', (filePath) => this.handleFileChange('removed', filePath));
|
|
103
|
+
}
|
|
104
|
+
handleFileChange(type, filePath) {
|
|
105
|
+
if (!filePath.endsWith('.json'))
|
|
106
|
+
return;
|
|
107
|
+
Logger_1.default.info(`Schema file ${type}: ${filePath}`);
|
|
108
|
+
try {
|
|
109
|
+
this.loadSchemas();
|
|
110
|
+
Logger_1.default.info('Schemas hot-reloaded successfully!');
|
|
111
|
+
this.emitter.emit('schemasChanged', this.getPublicSchemaList());
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
Logger_1.default.error(`Error during schema hot reload for ${filePath}:`, err);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
on(event, listener) {
|
|
118
|
+
this.emitter.on(event, listener);
|
|
119
|
+
}
|
|
120
|
+
loadSchemas() {
|
|
121
|
+
const schemas = {};
|
|
122
|
+
this.loadSchemasFromDirectory(this.packageSchemasDir, schemas, false, true);
|
|
123
|
+
if (fs_1.default.existsSync(this.userSchemasDir)) {
|
|
124
|
+
this.loadSchemasFromDirectory(this.userSchemasDir, schemas, true, false);
|
|
125
|
+
}
|
|
126
|
+
for (const schema of Object.values(schemas)) {
|
|
127
|
+
if (schema.extends) {
|
|
128
|
+
const baseSchema = schemas[schema.extends];
|
|
129
|
+
if (baseSchema) {
|
|
130
|
+
schema.attributes = {
|
|
131
|
+
...baseSchema.attributes,
|
|
132
|
+
...schema.attributes,
|
|
133
|
+
};
|
|
134
|
+
schema.allowedMethods = {
|
|
135
|
+
...baseSchema.allowedMethods,
|
|
136
|
+
...schema.allowedMethods,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
throw new Error(`Base schema ${schema.extends} not found for ${schema.modelName}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
this.processSchemas(schemas);
|
|
145
|
+
if (this.isDevelopment) {
|
|
146
|
+
Logger_1.default.info('Schemas loaded:', Object.keys(this.schemas).join(', '));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
processSchemas(schemas) {
|
|
150
|
+
this.schemas = Object.fromEntries(Object.entries(schemas).filter(([_, schema]) => !schema.private));
|
|
151
|
+
}
|
|
152
|
+
loadSchemasFromDirectory(directory, schemas, isUserSchema, isPackageSchema) {
|
|
153
|
+
if (!fs_1.default.existsSync(directory))
|
|
154
|
+
return;
|
|
155
|
+
const schemaFiles = fs_1.default
|
|
156
|
+
.readdirSync(directory)
|
|
157
|
+
.filter((file) => file.endsWith('.json'));
|
|
158
|
+
for (const file of schemaFiles) {
|
|
159
|
+
const schemaPath = path_1.default.join(directory, file);
|
|
160
|
+
try {
|
|
161
|
+
const raw = fs_1.default.readFileSync(schemaPath, 'utf-8');
|
|
162
|
+
const schema = JSON.parse(raw);
|
|
163
|
+
// VALIDATION
|
|
164
|
+
const missing = validateSchema(schema);
|
|
165
|
+
if (missing.length > 0) {
|
|
166
|
+
Logger_1.default.info(`Schema at ${schemaPath} is invalid. Missing/invalid fields: ${missing.join(', ')}. Skipping this schema.`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const modelName = schema.modelName;
|
|
170
|
+
if (this.nonOverridableSchemas.has(modelName)) {
|
|
171
|
+
if (isPackageSchema) {
|
|
172
|
+
schemas[modelName] = schema;
|
|
173
|
+
}
|
|
174
|
+
else if (isUserSchema) {
|
|
175
|
+
// Instead of blocking, MERGE user changes into package default
|
|
176
|
+
const base = schemas[modelName];
|
|
177
|
+
if (base) {
|
|
178
|
+
// shallow merge + attributes deep merge with delete support
|
|
179
|
+
const merged = {
|
|
180
|
+
...base,
|
|
181
|
+
...schema,
|
|
182
|
+
attributes: this.mergeAttributes(base.attributes, schema.attributes),
|
|
183
|
+
allowedMethods: {
|
|
184
|
+
...base.allowedMethods,
|
|
185
|
+
...schema.allowedMethods,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
schemas[modelName] = merged;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
schemas[modelName] = schema;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// current behavior for other models
|
|
197
|
+
if (isUserSchema || !schemas[modelName]) {
|
|
198
|
+
schemas[modelName] = schema;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
Logger_1.default.warn(`Schema for '${modelName}' already exists. Skipping ${schemaPath}.`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
Logger_1.default.error(`Error loading schema file ${schemaPath}:`, error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
mergeAttributes(base, patch) {
|
|
211
|
+
const out = { ...base };
|
|
212
|
+
for (const [k, v] of Object.entries(patch || {})) {
|
|
213
|
+
if (v &&
|
|
214
|
+
typeof v === 'object' &&
|
|
215
|
+
!Array.isArray(v) &&
|
|
216
|
+
v.$delete) {
|
|
217
|
+
delete out[k]; // allow deletions via {"$delete": true}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
out[k] = v; // override/relax required/unique/etc.
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
getSchemas() {
|
|
225
|
+
return this.schemas;
|
|
226
|
+
}
|
|
227
|
+
/** CLIENT/API: sanitized map with private attributes removed */
|
|
228
|
+
getPublicSchemas() {
|
|
229
|
+
const out = {};
|
|
230
|
+
for (const [name, s] of Object.entries(this.schemas)) {
|
|
231
|
+
// if (this.nonOverridableSchemas.has(name)) continue;
|
|
232
|
+
// Clone sanitized attributes and add timestamps
|
|
233
|
+
const attributesWithTimestamps = {
|
|
234
|
+
...this.sanitizeAttributes(s.attributes),
|
|
235
|
+
createdAt: { type: 'datetime' },
|
|
236
|
+
updatedAt: { type: 'datetime' },
|
|
237
|
+
};
|
|
238
|
+
out[name] = {
|
|
239
|
+
modelName: s.modelName,
|
|
240
|
+
allowedMethods: s.allowedMethods,
|
|
241
|
+
attributes: attributesWithTimestamps,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
/** CLIENT/API convenience: array of { modelName, attributes, allowedMethods } */
|
|
247
|
+
getPublicSchemaList() {
|
|
248
|
+
const pub = this.getPublicSchemas();
|
|
249
|
+
return Object.values(pub);
|
|
250
|
+
}
|
|
251
|
+
/** Strip any attr marked private; also remove the `private` flag from others */
|
|
252
|
+
/** Keep private/sensitive flags so the UI and policy layer can decide.
|
|
253
|
+
* We only shallow-clone values to avoid leaking references.
|
|
254
|
+
*/
|
|
255
|
+
sanitizeAttributes(attrs) {
|
|
256
|
+
const out = {};
|
|
257
|
+
if (!attrs || typeof attrs !== 'object' || Array.isArray(attrs))
|
|
258
|
+
return out;
|
|
259
|
+
for (const [key, attr] of Object.entries(attrs)) {
|
|
260
|
+
// Array attributes (e.g., relations) expect a single descriptor in [0]
|
|
261
|
+
if (Array.isArray(attr)) {
|
|
262
|
+
const elem = attr[0];
|
|
263
|
+
if (elem && typeof elem === 'object') {
|
|
264
|
+
// Keep as an array with a single shallow-cloned descriptor, preserving flags like `private`, `sensitive`, `writeOnly`
|
|
265
|
+
out[key] = [{ ...elem }];
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
// Fallback: keep as-is
|
|
269
|
+
out[key] = attr;
|
|
270
|
+
}
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
// Single attribute object
|
|
274
|
+
if (attr && typeof attr === 'object') {
|
|
275
|
+
// Keep `private`, `sensitive`, `writeOnly`, etc.
|
|
276
|
+
out[key] = { ...attr };
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
// Unexpected primitives — pass through
|
|
280
|
+
out[key] = attr;
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
closeWatcher() {
|
|
285
|
+
if (this.watcher) {
|
|
286
|
+
this.watcher.close();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
coerceIndexDir(val) {
|
|
290
|
+
if (val === true || val === 'asc' || val === 1)
|
|
291
|
+
return 1;
|
|
292
|
+
if (val === 'desc' || val === -1)
|
|
293
|
+
return -1;
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
/** Build desired single-field indexes per model from schemas.
|
|
297
|
+
* - respects attribute.index (true|1|-1|'asc'|'desc')
|
|
298
|
+
* - always adds createdAt/updatedAt (desc) to help common sorts.
|
|
299
|
+
*/
|
|
300
|
+
getIndexPlan() {
|
|
301
|
+
const plan = {};
|
|
302
|
+
for (const [name, s] of Object.entries(this.schemas)) {
|
|
303
|
+
const spec = {};
|
|
304
|
+
// from declared attributes
|
|
305
|
+
const attrs = s.attributes || {};
|
|
306
|
+
for (const [field, rawAttr] of Object.entries(attrs)) {
|
|
307
|
+
const attr = Array.isArray(rawAttr) ? rawAttr[0] : rawAttr;
|
|
308
|
+
if (!attr || typeof attr !== 'object')
|
|
309
|
+
continue;
|
|
310
|
+
// allow `index` on any attribute
|
|
311
|
+
const dir = this.coerceIndexDir(attr.index);
|
|
312
|
+
if (dir)
|
|
313
|
+
spec[field] = dir;
|
|
314
|
+
}
|
|
315
|
+
// always add timestamps (desc). Does not require attributes to exist.
|
|
316
|
+
spec.createdAt = spec.createdAt ?? -1;
|
|
317
|
+
spec.updatedAt = spec.updatedAt ?? -1;
|
|
318
|
+
plan[name] = spec;
|
|
319
|
+
}
|
|
320
|
+
return plan;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
exports.SchemaLoader = SchemaLoader;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateApiKey = generateApiKey;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
/**
|
|
9
|
+
* Generates a cryptographically strong API key.
|
|
10
|
+
* Uses 32 bytes (256 bits) hex string.
|
|
11
|
+
*/
|
|
12
|
+
function generateApiKey() {
|
|
13
|
+
return crypto_1.default.randomBytes(32).toString('hex');
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type Json = string | number | boolean | null | Json[] | {
|
|
2
|
+
[k: string]: Json;
|
|
3
|
+
};
|
|
4
|
+
/**
|
|
5
|
+
* DB-agnostic payload coercion:
|
|
6
|
+
* - time → "HH:mm"
|
|
7
|
+
* - date → "YYYY-MM-DD" (or "YYYY-MM-DDTHH:mm" when withTime)
|
|
8
|
+
* - range(timeOnly) → "HH:mm..HH:mm"
|
|
9
|
+
* - range(date[/time]) → "YYYY-MM-DD[..THH:mm]..YYYY-MM-DD[..THH:mm]"
|
|
10
|
+
*/
|
|
11
|
+
export declare function coerceForStorage(modelSchema: {
|
|
12
|
+
attributes: Record<string, any>;
|
|
13
|
+
}, payload: Record<string, any>): Record<string, Json>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.coerceForStorage = coerceForStorage;
|
|
4
|
+
function toTimeString(v) {
|
|
5
|
+
if (v == null)
|
|
6
|
+
return '';
|
|
7
|
+
let s = String(v).trim();
|
|
8
|
+
// 930 / 1530 → 09:30 / 15:30
|
|
9
|
+
if (/^\d{3,4}$/.test(s)) {
|
|
10
|
+
const pad = s.padStart(4, '0');
|
|
11
|
+
return `${pad.slice(0, 2)}:${pad.slice(2)}`;
|
|
12
|
+
}
|
|
13
|
+
// "h:mm AM/PM"
|
|
14
|
+
const ampm = s.match(/\s*([AaPp][Mm])\s*$/);
|
|
15
|
+
if (ampm) {
|
|
16
|
+
const core = s.replace(/\s*[AaPp][Mm]\s*$/, '').trim();
|
|
17
|
+
const m = core.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
|
|
18
|
+
if (!m)
|
|
19
|
+
return '';
|
|
20
|
+
let h = Number(m[1]);
|
|
21
|
+
const min = Number(m[2]);
|
|
22
|
+
const isPM = /^[Pp]/.test(ampm[1]);
|
|
23
|
+
if (isPM && h < 12)
|
|
24
|
+
h += 12;
|
|
25
|
+
if (!isPM && h === 12)
|
|
26
|
+
h = 0;
|
|
27
|
+
const hh = String(h).padStart(2, '0');
|
|
28
|
+
const mm = String(min).padStart(2, '0');
|
|
29
|
+
return `${hh}:${mm}`;
|
|
30
|
+
}
|
|
31
|
+
// "H:mm" / "HH:mm" / "HH:mm:ss"
|
|
32
|
+
const m24 = s.match(/^(\d{1,2}):(\d{2})(?::\d{2})?$/);
|
|
33
|
+
if (m24) {
|
|
34
|
+
const hh = String(Number(m24[1])).padStart(2, '0');
|
|
35
|
+
const mm = String(Number(m24[2])).padStart(2, '0');
|
|
36
|
+
return `${hh}:${mm}`;
|
|
37
|
+
}
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
function toDateString(v, withTime) {
|
|
41
|
+
if (v == null)
|
|
42
|
+
return '';
|
|
43
|
+
const s = String(v).trim();
|
|
44
|
+
// Already in target formats
|
|
45
|
+
if (!withTime && /^\d{4}-\d{2}-\d{2}$/.test(s))
|
|
46
|
+
return s;
|
|
47
|
+
if (withTime && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(s))
|
|
48
|
+
return s;
|
|
49
|
+
// Try to parse Date-ish strings or epoch numbers
|
|
50
|
+
const d = new Date(s);
|
|
51
|
+
if (Number.isFinite(Number(s))) {
|
|
52
|
+
const n = Number(s);
|
|
53
|
+
if (n > 0)
|
|
54
|
+
d.setTime(n);
|
|
55
|
+
}
|
|
56
|
+
if (isNaN(d.getTime()))
|
|
57
|
+
return '';
|
|
58
|
+
const yyyy = d.getFullYear();
|
|
59
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
60
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
61
|
+
if (!withTime)
|
|
62
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
63
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
64
|
+
const mi = String(d.getMinutes()).padStart(2, '0');
|
|
65
|
+
return `${yyyy}-${mm}-${dd}T${hh}:${mi}`;
|
|
66
|
+
}
|
|
67
|
+
function toRangeString(v, opts) {
|
|
68
|
+
if (v == null)
|
|
69
|
+
return '';
|
|
70
|
+
// Object {start,end}
|
|
71
|
+
if (typeof v === 'object' && !Array.isArray(v)) {
|
|
72
|
+
const anyv = v;
|
|
73
|
+
const s = anyv?.start ?? '';
|
|
74
|
+
const e = anyv?.end ?? '';
|
|
75
|
+
if (opts.timeOnly) {
|
|
76
|
+
return `${toTimeString(s)}..${toTimeString(e)}`;
|
|
77
|
+
}
|
|
78
|
+
return `${toDateString(s, !!opts.withTime)}..${toDateString(e, !!opts.withTime)}`;
|
|
79
|
+
}
|
|
80
|
+
// Array [start, end]
|
|
81
|
+
if (Array.isArray(v)) {
|
|
82
|
+
const [s, e] = v;
|
|
83
|
+
if (opts.timeOnly) {
|
|
84
|
+
return `${toTimeString(s)}..${toTimeString(e)}`;
|
|
85
|
+
}
|
|
86
|
+
return `${toDateString(s, !!opts.withTime)}..${toDateString(e, !!opts.withTime)}`;
|
|
87
|
+
}
|
|
88
|
+
// String "start..end" (leave as-is, but normalize empties)
|
|
89
|
+
if (typeof v === 'string') {
|
|
90
|
+
const [s = '', e = ''] = v.split('..');
|
|
91
|
+
if (opts.timeOnly)
|
|
92
|
+
return `${toTimeString(s)}..${toTimeString(e)}`;
|
|
93
|
+
return `${toDateString(s, !!opts.withTime)}..${toDateString(e, !!opts.withTime)}`;
|
|
94
|
+
}
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* DB-agnostic payload coercion:
|
|
99
|
+
* - time → "HH:mm"
|
|
100
|
+
* - date → "YYYY-MM-DD" (or "YYYY-MM-DDTHH:mm" when withTime)
|
|
101
|
+
* - range(timeOnly) → "HH:mm..HH:mm"
|
|
102
|
+
* - range(date[/time]) → "YYYY-MM-DD[..THH:mm]..YYYY-MM-DD[..THH:mm]"
|
|
103
|
+
*/
|
|
104
|
+
function coerceForStorage(modelSchema, payload) {
|
|
105
|
+
const out = { ...payload };
|
|
106
|
+
for (const [name, attr] of Object.entries(modelSchema.attributes ?? {})) {
|
|
107
|
+
if (!(name in out))
|
|
108
|
+
continue;
|
|
109
|
+
const t = String(attr?.type ?? '').toLowerCase();
|
|
110
|
+
const withTime = Boolean(attr?.withTime);
|
|
111
|
+
const timeOnly = Boolean(attr?.timeOnly);
|
|
112
|
+
const val = out[name];
|
|
113
|
+
switch (t) {
|
|
114
|
+
case 'time': {
|
|
115
|
+
out[name] = toTimeString(val);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 'date':
|
|
119
|
+
case 'datetime': {
|
|
120
|
+
out[name] = toDateString(val, withTime);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case 'range': {
|
|
124
|
+
out[name] = toRangeString(val, { timeOnly, withTime });
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
// leave as-is
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@airoom/nextmin-node",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "rm -rf dist && tsc --project tsconfig.json && copyfiles -u 2 \"src/schemas/**/*\" dist/schemas",
|
|
9
|
+
"watch": "tsc --project tsconfig.json --watch",
|
|
10
|
+
"prepublishOnly": "npm run build && npm pack --dry-run | (! grep -E '\\bsrc/|\\.map$')"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@aws-sdk/client-s3": "^3.864.0",
|
|
14
|
+
"@aws-sdk/s3-request-presigner": "^3.864.0",
|
|
15
|
+
"bcrypt": "^6.0.0",
|
|
16
|
+
"body-parser": "^2.2.0",
|
|
17
|
+
"chokidar": "^4.0.3",
|
|
18
|
+
"fast-glob": "^3.3.1",
|
|
19
|
+
"jsonwebtoken": "^9.0.2",
|
|
20
|
+
"kleur": "^4.1.5",
|
|
21
|
+
"mongoose": "^8.17.0",
|
|
22
|
+
"mongoose-autopopulate": "^1.1.0",
|
|
23
|
+
"multer": "^2.0.2",
|
|
24
|
+
"socket.io": "^4.7.5"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/bcrypt": "^6.0.0",
|
|
28
|
+
"@types/chokidar": "^2.1.7",
|
|
29
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
30
|
+
"@types/multer": "^2.0.0",
|
|
31
|
+
"copyfiles": "^2.4.1",
|
|
32
|
+
"typescript": "^5.3.3"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"package.json",
|
|
37
|
+
"tsconfig.json",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
}
|
|
45
|
+
}
|