mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(core): Support inserting dates to data store via the insert endpoint (#18404)
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
import { dataStoreColumnNameSchema } from '../../schemas/data-store.schema';
|
||||
import {
|
||||
dataStoreColumnNameSchema,
|
||||
dataStoreColumnValueSchema,
|
||||
} from '../../schemas/data-store.schema';
|
||||
|
||||
export class AddDataStoreRowsDto extends Z.class({
|
||||
data: z.array(z.record(dataStoreColumnNameSchema, z.any())),
|
||||
data: z.array(z.record(dataStoreColumnNameSchema, dataStoreColumnValueSchema)),
|
||||
}) {}
|
||||
|
||||
@@ -56,4 +56,5 @@ export {
|
||||
type DataStoreRows,
|
||||
type DataStoreListOptions,
|
||||
type DataStoreUserTableName,
|
||||
dateTimeSchema,
|
||||
} from './schemas/data-store.schema';
|
||||
|
||||
@@ -51,6 +51,15 @@ export type DataStoreListOptions = Partial<ListDataStoreQueryDto> & {
|
||||
filter: { projectId: string };
|
||||
};
|
||||
|
||||
export const dateTimeSchema = z
|
||||
.string()
|
||||
.datetime({ offset: true })
|
||||
.transform((s) => new Date(s))
|
||||
.pipe(z.date());
|
||||
|
||||
// Dates are received as date strings and validated before insertion
|
||||
export const dataStoreColumnValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
||||
|
||||
export type DataStoreColumnJsType = string | number | boolean | Date;
|
||||
|
||||
export type DataStoreRows = Array<Record<string, DataStoreColumnJsType | null>>;
|
||||
|
||||
@@ -119,6 +119,7 @@ exports[`Scope Information ensure scopes are defined correctly 1`] = `
|
||||
"dataStore:list",
|
||||
"dataStore:readRow",
|
||||
"dataStore:writeRow",
|
||||
"dataStore:listProject",
|
||||
"dataStore:*",
|
||||
"*",
|
||||
]
|
||||
|
||||
@@ -1940,6 +1940,214 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
|
||||
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {});
|
||||
expect(rowsInDb.count).toBe(0);
|
||||
});
|
||||
|
||||
test('should insert columns with dates', async () => {
|
||||
const dataStore = await createDataStore(memberProject, {
|
||||
columns: [
|
||||
{
|
||||
name: 'a',
|
||||
type: 'date',
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
type: 'date',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const payload = {
|
||||
data: [
|
||||
{
|
||||
a: '2025-08-15T09:48:14.259Z',
|
||||
b: '2025-08-15T12:34:56+02:00',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await authMemberAgent
|
||||
.post(`/projects/${memberProject.id}/data-stores/${dataStore.id}/insert`)
|
||||
.send(payload)
|
||||
.expect(200);
|
||||
|
||||
const readResponse = await authMemberAgent
|
||||
.get(`/projects/${memberProject.id}/data-stores/${dataStore.id}/rows`)
|
||||
.expect(200);
|
||||
|
||||
expect(readResponse.body.data.count).toBe(1);
|
||||
expect(readResponse.body.data.data[0]).toMatchObject({
|
||||
a: '2025-08-15T09:48:14.259Z',
|
||||
b: '2025-08-15T10:34:56.000Z',
|
||||
});
|
||||
});
|
||||
|
||||
test('should insert columns with strings', async () => {
|
||||
const dataStore = await createDataStore(memberProject, {
|
||||
columns: [
|
||||
{
|
||||
name: 'a',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const payload = {
|
||||
data: [
|
||||
{
|
||||
a: 'some string',
|
||||
b: '',
|
||||
c: '2025-08-15T09:48:14.259Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await authMemberAgent
|
||||
.post(`/projects/${memberProject.id}/data-stores/${dataStore.id}/insert`)
|
||||
.send(payload)
|
||||
.expect(200);
|
||||
|
||||
const readResponse = await authMemberAgent
|
||||
.get(`/projects/${memberProject.id}/data-stores/${dataStore.id}/rows`)
|
||||
.expect(200);
|
||||
|
||||
expect(readResponse.body.data.count).toBe(1);
|
||||
expect(readResponse.body.data.data[0]).toMatchObject(payload.data[0]);
|
||||
});
|
||||
|
||||
test('should insert columns with booleans', async () => {
|
||||
const dataStore = await createDataStore(memberProject, {
|
||||
columns: [
|
||||
{
|
||||
name: 'a',
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
type: 'boolean',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const payload = {
|
||||
data: [
|
||||
{
|
||||
a: true,
|
||||
b: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await authMemberAgent
|
||||
.post(`/projects/${memberProject.id}/data-stores/${dataStore.id}/insert`)
|
||||
.send(payload)
|
||||
.expect(200);
|
||||
|
||||
const readResponse = await authMemberAgent
|
||||
.get(`/projects/${memberProject.id}/data-stores/${dataStore.id}/rows`)
|
||||
.expect(200);
|
||||
|
||||
expect(readResponse.body.data.count).toBe(1);
|
||||
expect(readResponse.body.data.data[0]).toMatchObject(payload.data[0]);
|
||||
});
|
||||
|
||||
test('should insert columns with numbers', async () => {
|
||||
const dataStore = await createDataStore(memberProject, {
|
||||
columns: [
|
||||
{
|
||||
name: 'a',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const payload = {
|
||||
data: [
|
||||
{
|
||||
a: 1,
|
||||
b: 0,
|
||||
c: -1,
|
||||
d: 0.2340439341231259,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await authMemberAgent
|
||||
.post(`/projects/${memberProject.id}/data-stores/${dataStore.id}/insert`)
|
||||
.send(payload)
|
||||
.expect(200);
|
||||
|
||||
const readResponse = await authMemberAgent
|
||||
.get(`/projects/${memberProject.id}/data-stores/${dataStore.id}/rows`)
|
||||
.expect(200);
|
||||
|
||||
expect(readResponse.body.data.count).toBe(1);
|
||||
expect(readResponse.body.data.data[0]).toMatchObject(payload.data[0]);
|
||||
});
|
||||
|
||||
test('should insert columns with null values', async () => {
|
||||
const dataStore = await createDataStore(memberProject, {
|
||||
columns: [
|
||||
{
|
||||
name: 'a',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
type: 'date',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const payload = {
|
||||
data: [
|
||||
{
|
||||
a: null,
|
||||
b: null,
|
||||
c: null,
|
||||
d: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await authMemberAgent
|
||||
.post(`/projects/${memberProject.id}/data-stores/${dataStore.id}/insert`)
|
||||
.send(payload)
|
||||
.expect(200);
|
||||
|
||||
const readResponse = await authMemberAgent
|
||||
.get(`/projects/${memberProject.id}/data-stores/${dataStore.id}/rows`)
|
||||
.expect(200);
|
||||
|
||||
expect(readResponse.body.data.count).toBe(1);
|
||||
expect(readResponse.body.data.data[0]).toMatchObject(payload.data[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /projects/:projectId/data-stores/:dataStoreId/rows', () => {
|
||||
|
||||
@@ -875,6 +875,12 @@ describe('dataStore', () => {
|
||||
{ c1: 3, c2: true, c3: new Date(), c4: 'hello?' },
|
||||
{ c1: 4, c2: false, c3: new Date(), c4: 'hello!' },
|
||||
{ c1: 5, c2: true, c3: new Date(), c4: 'hello.' },
|
||||
{
|
||||
c1: 1,
|
||||
c2: true,
|
||||
c3: '2025-08-15T09:48:14.259Z',
|
||||
c4: 'iso 8601 date strings are okay too',
|
||||
},
|
||||
];
|
||||
const result = await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
||||
|
||||
@@ -886,7 +892,7 @@ describe('dataStore', () => {
|
||||
project1.id,
|
||||
{},
|
||||
);
|
||||
expect(count).toEqual(3);
|
||||
expect(count).toEqual(4);
|
||||
expect(data).toEqual(
|
||||
rows.map((row, i) => ({
|
||||
...row,
|
||||
@@ -998,6 +1004,26 @@ describe('dataStore', () => {
|
||||
await expect(result).rejects.toThrow(new DataStoreValidationError('unknown column name'));
|
||||
});
|
||||
|
||||
it('rejects a invalid date string to date column', async () => {
|
||||
// ARRANGE
|
||||
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'dataStore',
|
||||
columns: [{ name: 'c1', type: 'date' }],
|
||||
});
|
||||
|
||||
// ACT
|
||||
const result = dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||
{ c1: '2025-99-15T09:48:14.259Z' },
|
||||
]);
|
||||
|
||||
// ASSERT
|
||||
await expect(result).rejects.toThrow(
|
||||
new DataStoreValidationError(
|
||||
"value '2025-99-15T09:48:14.259Z' does not match column type 'date'",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects unknown data store id', async () => {
|
||||
// ARRANGE
|
||||
await dataStoreService.createDataStore(project1.id, {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { dateTimeSchema } from '@n8n/api-types';
|
||||
import type {
|
||||
AddDataStoreColumnDto,
|
||||
CreateDataStoreDto,
|
||||
@@ -172,29 +173,38 @@ export class DataStoreService {
|
||||
if (cell === null) continue;
|
||||
switch (columnTypeMap.get(key)) {
|
||||
case 'boolean':
|
||||
if (typeof cell !== 'boolean')
|
||||
if (typeof cell !== 'boolean') {
|
||||
throw new DataStoreValidationError(
|
||||
`value '${cell.toString()}' does not match column type 'boolean'`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'date':
|
||||
if (!(cell instanceof Date))
|
||||
throw new DataStoreValidationError(
|
||||
`value '${cell}' does not match column type 'date'`,
|
||||
);
|
||||
row[key] = cell.toISOString();
|
||||
break;
|
||||
if (typeof cell === 'string') {
|
||||
const validated = dateTimeSchema.safeParse(cell);
|
||||
if (validated.success) {
|
||||
row[key] = validated.data.toISOString();
|
||||
break;
|
||||
}
|
||||
} else if (cell instanceof Date) {
|
||||
row[key] = cell.toISOString();
|
||||
break;
|
||||
}
|
||||
|
||||
throw new DataStoreValidationError(`value '${cell}' does not match column type 'date'`);
|
||||
case 'string':
|
||||
if (typeof cell !== 'string')
|
||||
if (typeof cell !== 'string') {
|
||||
throw new DataStoreValidationError(
|
||||
`value '${cell.toString()}' does not match column type 'string'`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'number':
|
||||
if (typeof cell !== 'number')
|
||||
if (typeof cell !== 'number') {
|
||||
throw new DataStoreValidationError(
|
||||
`value '${cell.toString()}' does not match column type 'number'`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user