mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +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';
|
||||||
import { Z } from 'zod-class';
|
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({
|
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 DataStoreRows,
|
||||||
type DataStoreListOptions,
|
type DataStoreListOptions,
|
||||||
type DataStoreUserTableName,
|
type DataStoreUserTableName,
|
||||||
|
dateTimeSchema,
|
||||||
} from './schemas/data-store.schema';
|
} from './schemas/data-store.schema';
|
||||||
|
|||||||
@@ -51,6 +51,15 @@ export type DataStoreListOptions = Partial<ListDataStoreQueryDto> & {
|
|||||||
filter: { projectId: string };
|
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 DataStoreColumnJsType = string | number | boolean | Date;
|
||||||
|
|
||||||
export type DataStoreRows = Array<Record<string, DataStoreColumnJsType | null>>;
|
export type DataStoreRows = Array<Record<string, DataStoreColumnJsType | null>>;
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ exports[`Scope Information ensure scopes are defined correctly 1`] = `
|
|||||||
"dataStore:list",
|
"dataStore:list",
|
||||||
"dataStore:readRow",
|
"dataStore:readRow",
|
||||||
"dataStore:writeRow",
|
"dataStore:writeRow",
|
||||||
|
"dataStore:listProject",
|
||||||
"dataStore:*",
|
"dataStore:*",
|
||||||
"*",
|
"*",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1940,6 +1940,214 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
|
|||||||
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {});
|
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {});
|
||||||
expect(rowsInDb.count).toBe(0);
|
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', () => {
|
describe('DELETE /projects/:projectId/data-stores/:dataStoreId/rows', () => {
|
||||||
|
|||||||
@@ -875,6 +875,12 @@ describe('dataStore', () => {
|
|||||||
{ c1: 3, c2: true, c3: new Date(), c4: 'hello?' },
|
{ c1: 3, c2: true, c3: new Date(), c4: 'hello?' },
|
||||||
{ c1: 4, c2: false, c3: new Date(), c4: 'hello!' },
|
{ c1: 4, c2: false, c3: new Date(), c4: 'hello!' },
|
||||||
{ c1: 5, c2: true, 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);
|
const result = await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
||||||
|
|
||||||
@@ -886,7 +892,7 @@ describe('dataStore', () => {
|
|||||||
project1.id,
|
project1.id,
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
expect(count).toEqual(3);
|
expect(count).toEqual(4);
|
||||||
expect(data).toEqual(
|
expect(data).toEqual(
|
||||||
rows.map((row, i) => ({
|
rows.map((row, i) => ({
|
||||||
...row,
|
...row,
|
||||||
@@ -998,6 +1004,26 @@ describe('dataStore', () => {
|
|||||||
await expect(result).rejects.toThrow(new DataStoreValidationError('unknown column name'));
|
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 () => {
|
it('rejects unknown data store id', async () => {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
await dataStoreService.createDataStore(project1.id, {
|
await dataStoreService.createDataStore(project1.id, {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { dateTimeSchema } from '@n8n/api-types';
|
||||||
import type {
|
import type {
|
||||||
AddDataStoreColumnDto,
|
AddDataStoreColumnDto,
|
||||||
CreateDataStoreDto,
|
CreateDataStoreDto,
|
||||||
@@ -172,29 +173,38 @@ export class DataStoreService {
|
|||||||
if (cell === null) continue;
|
if (cell === null) continue;
|
||||||
switch (columnTypeMap.get(key)) {
|
switch (columnTypeMap.get(key)) {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
if (typeof cell !== 'boolean')
|
if (typeof cell !== 'boolean') {
|
||||||
throw new DataStoreValidationError(
|
throw new DataStoreValidationError(
|
||||||
`value '${cell.toString()}' does not match column type 'boolean'`,
|
`value '${cell.toString()}' does not match column type 'boolean'`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'date':
|
case 'date':
|
||||||
if (!(cell instanceof Date))
|
if (typeof cell === 'string') {
|
||||||
throw new DataStoreValidationError(
|
const validated = dateTimeSchema.safeParse(cell);
|
||||||
`value '${cell}' does not match column type 'date'`,
|
if (validated.success) {
|
||||||
);
|
row[key] = validated.data.toISOString();
|
||||||
row[key] = cell.toISOString();
|
break;
|
||||||
break;
|
}
|
||||||
|
} else if (cell instanceof Date) {
|
||||||
|
row[key] = cell.toISOString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DataStoreValidationError(`value '${cell}' does not match column type 'date'`);
|
||||||
case 'string':
|
case 'string':
|
||||||
if (typeof cell !== 'string')
|
if (typeof cell !== 'string') {
|
||||||
throw new DataStoreValidationError(
|
throw new DataStoreValidationError(
|
||||||
`value '${cell.toString()}' does not match column type 'string'`,
|
`value '${cell.toString()}' does not match column type 'string'`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'number':
|
case 'number':
|
||||||
if (typeof cell !== 'number')
|
if (typeof cell !== 'number') {
|
||||||
throw new DataStoreValidationError(
|
throw new DataStoreValidationError(
|
||||||
`value '${cell.toString()}' does not match column type 'number'`,
|
`value '${cell.toString()}' does not match column type 'number'`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user