From c568d96406291bb5e82f554c257e78ec1f94b3f2 Mon Sep 17 00:00:00 2001 From: Abhishek Chatterjee Date: Sun, 17 May 2026 19:50:33 +0530 Subject: [PATCH] feat(datatype): ROSS-92: add support for date data type across configuration, validation, and database migration layers --- src/interfaces/config.ts | 3 ++- src/migrator/index.ts | 8 +++++++- src/routes/custom-queries/custom-queries.ts | 2 ++ src/routes/schema-helpers.ts | 6 ++++++ src/validators/config/schema.ts | 10 +++++++++- src/validators/config/validate-model.ts | 21 +++++++++++++++++++- tests/migrator/index.test.ts | 2 +- tests/routes/custom-queries.test.ts | 3 ++- tests/routes/schema-helper.test.ts | 1 + tests/validators/config.test.ts | 22 +++++++++++++++++++++ 10 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 322f0b9..c7f4031 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -8,7 +8,8 @@ export type DataType = | 'boolean' | 'text' | 'datetime' - | 'decimal'; + | 'decimal' + | 'date'; export type LogLevel = | 'trace' | 'debug' diff --git a/src/migrator/index.ts b/src/migrator/index.ts index 5ccd78c..d501ce3 100644 --- a/src/migrator/index.ts +++ b/src/migrator/index.ts @@ -45,6 +45,9 @@ function generateSchemaFile(config: ModelConfig, engine: DBEngine): string { case 'decimal': col = `real('${f.name}')`; break; + case 'date': + col = `text('${f.name}')`; + break; default: col = `text('${f.name}')`; break; // fallback @@ -102,6 +105,9 @@ ${columns} case 'decimal': col = `doublePrecision('${f.name}')`; break; + case 'date': + col = `date('${f.name}')`; + break; default: col = `text('${f.name}')`; break; // fallback @@ -132,7 +138,7 @@ ${columns} const extras = [indexes, foreignKeys].filter(Boolean).join(',\n'); return ` -import { pgTable, serial, integer, text, boolean, doublePrecision, index, uniqueIndex, timestamp, foreignKey } from 'drizzle-orm/pg-core'; +import { pgTable, serial, integer, text, boolean, doublePrecision, index, uniqueIndex, timestamp, date, foreignKey } from 'drizzle-orm/pg-core'; export const ${config.name} = pgTable('${config.name}', { ${columns} diff --git a/src/routes/custom-queries/custom-queries.ts b/src/routes/custom-queries/custom-queries.ts index 3b21f2b..3224b69 100644 --- a/src/routes/custom-queries/custom-queries.ts +++ b/src/routes/custom-queries/custom-queries.ts @@ -35,6 +35,8 @@ const cast = (value: unknown, type: DataType): unknown => { return String(value); case 'datetime': return String(value); + case 'date': + return String(value); case 'decimal': return Number(value); default: diff --git a/src/routes/schema-helpers.ts b/src/routes/schema-helpers.ts index cd58de8..f27b025 100644 --- a/src/routes/schema-helpers.ts +++ b/src/routes/schema-helpers.ts @@ -24,6 +24,8 @@ export function mapDataTypeToJsonSchema(type: DataType): { return {type: 'string'}; case 'datetime': return {type: 'string', format: 'date-time'}; + case 'date': + return {type: 'string', format: 'date'}; case 'decimal': return {type: 'number'}; default: @@ -141,6 +143,10 @@ function normalizeSchemaForAjv(schema: JsonSchemaObject): JsonSchemaObject { prop.type = 'string'; prop.format = 'date-time'; } + if (prop && prop.type === 'date') { + prop.type = 'string'; + prop.format = 'date'; + } }); } return normalized; diff --git a/src/validators/config/schema.ts b/src/validators/config/schema.ts index a49d0bd..ef1395a 100644 --- a/src/validators/config/schema.ts +++ b/src/validators/config/schema.ts @@ -184,7 +184,15 @@ const fieldSchema = { }, type: { type: 'string', - enum: ['integer', 'string', 'boolean', 'text', 'datetime', 'decimal'], + enum: [ + 'integer', + 'string', + 'boolean', + 'text', + 'datetime', + 'decimal', + 'date', + ], }, primaryKey: {type: 'boolean', default: false}, nullable: {type: 'boolean', default: true}, diff --git a/src/validators/config/validate-model.ts b/src/validators/config/validate-model.ts index f0bc291..dd9aa2a 100644 --- a/src/validators/config/validate-model.ts +++ b/src/validators/config/validate-model.ts @@ -51,6 +51,15 @@ const ALLOWED_OPERATIONS: Record = { 'equal', 'oneOf', ], + date: [ + 'sortable', + 'lessThan', + 'lessThanEqual', + 'greaterThan', + 'greaterThanEqual', + 'equal', + 'oneOf', + ], }; const ALLOWED_AGGREGATIONS: Record = { @@ -60,6 +69,7 @@ const ALLOWED_AGGREGATIONS: Record = { boolean: ['count', 'frequency'], text: [], datetime: ['mean', 'max', 'min', 'count'], + date: ['mean', 'max', 'min', 'count'], }; function mapModelTypeToJsonSchema(type: string): string { @@ -75,6 +85,8 @@ function mapModelTypeToJsonSchema(type: string): string { return 'boolean'; case 'datetime': return 'date-time'; + case 'date': + return 'date'; /* istanbul ignore next */ default: return 'string'; @@ -92,6 +104,10 @@ function normalizeSchemaForAjv(schema: JsonSchemaObject): JsonSchemaObject { prop.type = 'string'; prop.format = 'date-time'; } + if (prop && prop.type === 'date') { + prop.type = 'string'; + prop.format = 'date'; + } }); } return normalized; @@ -201,7 +217,10 @@ function validateModelValidation(config: AppConfig, ajv: Ajv): string[] { (schemaType === 'datetime' || schemaType === 'date-time') && (expectedType === 'datetime' || expectedType === 'date-time'); - if (!isDateMatch) { + const isJustDateMatch = + schemaType === 'date' && expectedType === 'date'; + + if (!isDateMatch && !isJustDateMatch) { errors.push( `${propPath}: type mismatch (model=${modelType}, schema=${schemaType})`, ); diff --git a/tests/migrator/index.test.ts b/tests/migrator/index.test.ts index b95952a..cdd19fb 100644 --- a/tests/migrator/index.test.ts +++ b/tests/migrator/index.test.ts @@ -137,7 +137,7 @@ describe('migrateDatabase', () => { const schemaContent = writeFileSyncMock.mock.calls[0][1] as string; expect(schemaContent).toContain( - "import { pgTable, serial, integer, text, boolean, doublePrecision, index, uniqueIndex, timestamp, foreignKey } from 'drizzle-orm/pg-core'", + "import { pgTable, serial, integer, text, boolean, doublePrecision, index, uniqueIndex, timestamp, date, foreignKey } from 'drizzle-orm/pg-core'", ); expect(schemaContent).toContain("export const posts = pgTable('posts'"); expect(schemaContent).toContain("id: serial('id').primaryKey()"); diff --git a/tests/routes/custom-queries.test.ts b/tests/routes/custom-queries.test.ts index 950489a..c841d9e 100644 --- a/tests/routes/custom-queries.test.ts +++ b/tests/routes/custom-queries.test.ts @@ -88,7 +88,7 @@ describe('test custom-queries api', () => { method: 'POST', path: '/all-types', query: - 'INSERT INTO test (b, t, d, dec) VALUES (@@b:boolean@@, @@t:text@@, @@d:datetime@@, @@dec:decimal@@);', + 'INSERT INTO test (b, t, d, dec, dt) VALUES (@@b:boolean@@, @@t:text@@, @@d:datetime@@, @@dec:decimal@@, @@dt:date@@);', }, ], }; @@ -107,6 +107,7 @@ describe('test custom-queries api', () => { t: 'some long text', d: '2023-01-01T00:00:00Z', dec: 12.34, + dt: '2023-01-01', }, }); diff --git a/tests/routes/schema-helper.test.ts b/tests/routes/schema-helper.test.ts index 43d5040..15cd58a 100644 --- a/tests/routes/schema-helper.test.ts +++ b/tests/routes/schema-helper.test.ts @@ -23,6 +23,7 @@ describe('test schema helper', () => { expectedSchema: {type: 'string', format: 'date-time'}, }, {dataType: 'decimal', expectedSchema: {type: 'number'}}, + {dataType: 'date', expectedSchema: {type: 'string', format: 'date'}}, {dataType: 'array', expectedSchema: {type: 'string'}}, {dataType: 'null', expectedSchema: {type: 'string'}}, ])('should map $dataType to JSON schema', ({dataType, expectedSchema}) => { diff --git a/tests/validators/config.test.ts b/tests/validators/config.test.ts index 2edc494..a92cad9 100644 --- a/tests/validators/config.test.ts +++ b/tests/validators/config.test.ts @@ -570,6 +570,17 @@ describe('validateInvalidModelFieldsConfig', () => { expected: '/models/0/fields/0/supportedOperations: "searchable" is not allowed for type "decimal"', }, + { + name: 'field.supportedOperations contains invalid value for type=date', + patch: { + name: 'test', + fields: [ + {name: 'test', type: 'date', supportedOperations: ['searchable']}, + ], + }, + expected: + '/models/0/fields/0/supportedOperations: "searchable" is not allowed for type "date"', + }, { name: 'field.supportedOperations contains invalid value for type=string', patch: { @@ -949,6 +960,17 @@ describe('validateInvalidModelFieldsConfig', () => { expected: '/models/0/fields/0/supportedAggregation: "frequency" is not allowed for type "decimal"', }, + { + name: 'field.supportedAggregation contains invalid value for type=date', + patch: { + name: 'test', + fields: [ + {name: 'test', type: 'date', supportedAggregation: ['frequency']}, + ], + }, + expected: + '/models/0/fields/0/supportedAggregation: "frequency" is not allowed for type "date"', + }, { name: 'field.supportedAggregation contains invalid value for type=string', patch: {