mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat(core): Optionally return full rows from Data Table inserts (no-changelog) (#18657)
This commit is contained in:
@@ -7,5 +7,6 @@ import {
|
|||||||
} from '../../schemas/data-store.schema';
|
} from '../../schemas/data-store.schema';
|
||||||
|
|
||||||
export class AddDataStoreRowsDto extends Z.class({
|
export class AddDataStoreRowsDto extends Z.class({
|
||||||
|
returnData: z.boolean().default(false),
|
||||||
data: z.array(z.record(dataStoreColumnNameSchema, dataStoreColumnValueSchema)),
|
data: z.array(z.record(dataStoreColumnNameSchema, dataStoreColumnValueSchema)),
|
||||||
}) {}
|
}) {}
|
||||||
|
|||||||
@@ -1835,7 +1835,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toEqual({
|
||||||
data: [1],
|
data: [{ id: 1 }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
|
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
|
||||||
@@ -1875,7 +1875,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toEqual({
|
||||||
data: [1],
|
data: [{ id: 1 }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
|
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
|
||||||
@@ -1912,7 +1912,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toEqual({
|
||||||
data: [1],
|
data: [{ id: 1 }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
|
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
|
||||||
@@ -1920,6 +1920,59 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
|
|||||||
expect(rowsInDb.data[0]).toMatchObject(payload.data[0]);
|
expect(rowsInDb.data[0]).toMatchObject(payload.data[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should return inserted data if returnData is set', async () => {
|
||||||
|
const dataStore = await createDataStore(memberProject, {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'first',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'second',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
returnData: true,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
first: 'first row',
|
||||||
|
second: 'some value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
first: 'another row',
|
||||||
|
second: 'another value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await authMemberAgent
|
||||||
|
.post(`/projects/${memberProject.id}/data-stores/${dataStore.id}/insert`)
|
||||||
|
.send(payload)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
first: 'first row',
|
||||||
|
second: 'some value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
first: 'another row',
|
||||||
|
second: 'another value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
|
||||||
|
expect(rowsInDb.count).toBe(2);
|
||||||
|
expect(rowsInDb.data[0]).toMatchObject(payload.data[0]);
|
||||||
|
});
|
||||||
|
|
||||||
test('should not insert rows when column does not exist', async () => {
|
test('should not insert rows when column does not exist', async () => {
|
||||||
const dataStore = await createDataStore(memberProject, {
|
const dataStore = await createDataStore(memberProject, {
|
||||||
columns: [
|
columns: [
|
||||||
@@ -1982,7 +2035,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toEqual({
|
||||||
data: [1],
|
data: [{ id: 1 }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const readResponse = await authMemberAgent
|
const readResponse = await authMemberAgent
|
||||||
@@ -2030,7 +2083,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toEqual({
|
||||||
data: [1],
|
data: [{ id: 1 }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const readResponse = await authMemberAgent
|
const readResponse = await authMemberAgent
|
||||||
@@ -2070,7 +2123,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toEqual({
|
||||||
data: [1],
|
data: [{ id: 1 }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const readResponse = await authMemberAgent
|
const readResponse = await authMemberAgent
|
||||||
@@ -2125,7 +2178,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toEqual({
|
||||||
data: [1],
|
data: [{ id: 1 }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const readResponse = await authMemberAgent
|
const readResponse = await authMemberAgent
|
||||||
@@ -2175,7 +2228,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toEqual({
|
||||||
data: [1],
|
data: [{ id: 1 }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const readResponse = await authMemberAgent
|
const readResponse = await authMemberAgent
|
||||||
@@ -2223,7 +2276,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(first.body).toEqual({
|
expect(first.body).toEqual({
|
||||||
data: [1, 2, 3],
|
data: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const second = await authMemberAgent
|
const second = await authMemberAgent
|
||||||
@@ -2232,7 +2285,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(second.body).toEqual({
|
expect(second.body).toEqual({
|
||||||
data: [4, 5, 6],
|
data: [{ id: 4 }, { id: 5 }, { id: 6 }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const readResponse = await authMemberAgent
|
const readResponse = await authMemberAgent
|
||||||
|
|||||||
@@ -462,7 +462,17 @@ describe('dataStore', () => {
|
|||||||
{ name: 'Charlie', age: 35 },
|
{ name: 'Charlie', age: 35 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(results).toEqual([1, 2, 3]);
|
expect(results).toEqual([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
const newColumn = await dataStoreService.addColumn(dataStoreId, project1.id, {
|
const newColumn = await dataStoreService.addColumn(dataStoreId, project1.id, {
|
||||||
@@ -491,7 +501,11 @@ describe('dataStore', () => {
|
|||||||
const newRow = await dataStoreService.insertRows(dataStoreId, project1.id, [
|
const newRow = await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||||
{ name: 'David', age: 28, email: 'david@example.com' },
|
{ name: 'David', age: 28, email: 'david@example.com' },
|
||||||
]);
|
]);
|
||||||
expect(newRow).toEqual([4]);
|
expect(newRow).toEqual([
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const finalData = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {});
|
const finalData = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {});
|
||||||
expect(finalData.count).toBe(4);
|
expect(finalData.count).toBe(4);
|
||||||
@@ -975,7 +989,7 @@ describe('dataStore', () => {
|
|||||||
const result = await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
const result = await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
expect(result).toEqual([1, 2, 3, 4]);
|
expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]);
|
||||||
|
|
||||||
const { count, data } = await dataStoreService.getManyRowsAndCount(
|
const { count, data } = await dataStoreService.getManyRowsAndCount(
|
||||||
dataStoreId,
|
dataStoreId,
|
||||||
@@ -1009,7 +1023,7 @@ describe('dataStore', () => {
|
|||||||
const initial = await dataStoreService.insertRows(dataStoreId, project1.id, [
|
const initial = await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||||
{ c1: 1, c2: 'foo' },
|
{ c1: 1, c2: 'foo' },
|
||||||
]);
|
]);
|
||||||
expect(initial).toEqual([1]);
|
expect(initial).toEqual([{ id: 1 }]);
|
||||||
|
|
||||||
// Attempt to insert a row with the same primary key
|
// Attempt to insert a row with the same primary key
|
||||||
const result = await dataStoreService.insertRows(dataStoreId, project1.id, [
|
const result = await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||||
@@ -1017,7 +1031,7 @@ describe('dataStore', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
expect(result).toEqual([2]);
|
expect(result).toEqual([{ id: 2 }]);
|
||||||
|
|
||||||
const { count, data } = await dataStoreRowsRepository.getManyAndCount(dataStoreId, {});
|
const { count, data } = await dataStoreRowsRepository.getManyAndCount(dataStoreId, {});
|
||||||
|
|
||||||
@@ -1043,9 +1057,9 @@ describe('dataStore', () => {
|
|||||||
{ c1: 1, c2: 'foo' },
|
{ c1: 1, c2: 'foo' },
|
||||||
{ c1: 2, c2: 'bar' },
|
{ c1: 2, c2: 'bar' },
|
||||||
]);
|
]);
|
||||||
expect(ids).toEqual([1, 2]);
|
expect(ids).toEqual([{ id: 1 }, { id: 2 }]);
|
||||||
|
|
||||||
await dataStoreService.deleteRows(dataStoreId, project1.id, [ids[0]]);
|
await dataStoreService.deleteRows(dataStoreId, project1.id, [ids[0].id]);
|
||||||
|
|
||||||
// Insert a new row
|
// Insert a new row
|
||||||
const result = await dataStoreService.insertRows(dataStoreId, project1.id, [
|
const result = await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||||
@@ -1054,7 +1068,7 @@ describe('dataStore', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
expect(result).toEqual([3, 4]);
|
expect(result).toEqual([{ id: 3 }, { id: 4 }]);
|
||||||
|
|
||||||
const { count, data } = await dataStoreRowsRepository.getManyAndCount(dataStoreId, {});
|
const { count, data } = await dataStoreRowsRepository.getManyAndCount(dataStoreId, {});
|
||||||
|
|
||||||
@@ -1066,6 +1080,50 @@ describe('dataStore', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('return inserted data if requested', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
|
||||||
|
name: 'myDataStore',
|
||||||
|
columns: [
|
||||||
|
{ name: 'c1', type: 'number' },
|
||||||
|
{ name: 'c2', type: 'string' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert initial row
|
||||||
|
const ids = await dataStoreService.insertRows(
|
||||||
|
dataStoreId,
|
||||||
|
project1.id,
|
||||||
|
[
|
||||||
|
{ c1: 1, c2: 'foo' },
|
||||||
|
{ c1: 2, c2: 'bar' },
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(ids).toEqual([
|
||||||
|
{ id: 1, c1: 1, c2: 'foo' },
|
||||||
|
{ id: 2, c1: 2, c2: 'bar' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
await dataStoreService.deleteRows(dataStoreId, project1.id, [ids[0].id]);
|
||||||
|
|
||||||
|
const result = await dataStoreService.insertRows(
|
||||||
|
dataStoreId,
|
||||||
|
project1.id,
|
||||||
|
[
|
||||||
|
{ c1: 1, c2: 'baz' },
|
||||||
|
{ c1: 2, c2: 'faz' },
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ id: 3, c1: 1, c2: 'baz' },
|
||||||
|
{ id: 4, c1: 2, c2: 'faz' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects a mismatched row with extra column', async () => {
|
it('rejects a mismatched row with extra column', async () => {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
|
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
|
||||||
@@ -1205,7 +1263,7 @@ describe('dataStore', () => {
|
|||||||
const result = await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
const result = await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
expect(result).toEqual([1, 2, 3]);
|
expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
|
||||||
|
|
||||||
const { count, data } = await dataStoreService.getManyRowsAndCount(
|
const { count, data } = await dataStoreService.getManyRowsAndCount(
|
||||||
dataStoreId,
|
dataStoreId,
|
||||||
@@ -1229,12 +1287,14 @@ describe('dataStore', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await dataStoreService.insertRows(dataStoreId, project1.id, [
|
const ids = await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||||
{ pid: '1995-111a', name: 'Alice', age: 30 },
|
{ pid: '1995-111a', name: 'Alice', age: 30 },
|
||||||
{ pid: '1994-222a', name: 'John', age: 31 },
|
{ pid: '1994-222a', name: 'John', age: 31 },
|
||||||
{ pid: '1993-333a', name: 'Paul', age: 32 },
|
{ pid: '1993-333a', name: 'Paul', age: 32 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
expect(ids).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
const result = await dataStoreService.upsertRows(dataStoreId, project1.id, {
|
const result = await dataStoreService.upsertRows(dataStoreId, project1.id, {
|
||||||
rows: [
|
rows: [
|
||||||
@@ -1323,7 +1383,7 @@ describe('dataStore', () => {
|
|||||||
const ids = await dataStoreService.insertRows(dataStoreId, project1.id, [
|
const ids = await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||||
{ pid: '1995-111a', name: 'Alice', age: 30 },
|
{ pid: '1995-111a', name: 'Alice', age: 30 },
|
||||||
]);
|
]);
|
||||||
expect(ids).toEqual([1]);
|
expect(ids).toEqual([{ id: 1 }]);
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
const result = await dataStoreService.upsertRows(dataStoreId, project1.id, {
|
const result = await dataStoreService.upsertRows(dataStoreId, project1.id, {
|
||||||
@@ -1366,7 +1426,7 @@ describe('dataStore', () => {
|
|||||||
{ name: 'Bob', age: 25 },
|
{ name: 'Bob', age: 25 },
|
||||||
{ name: 'Charlie', age: 35 },
|
{ name: 'Charlie', age: 35 },
|
||||||
]);
|
]);
|
||||||
expect(ids).toEqual([1, 2, 3]);
|
expect(ids).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
|
||||||
|
|
||||||
// Get initial data to find row IDs
|
// Get initial data to find row IDs
|
||||||
const initialData = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {});
|
const initialData = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {});
|
||||||
@@ -1415,7 +1475,7 @@ describe('dataStore', () => {
|
|||||||
|
|
||||||
// Insert one row
|
// Insert one row
|
||||||
const ids = await dataStoreService.insertRows(dataStoreId, project1.id, [{ name: 'Alice' }]);
|
const ids = await dataStoreService.insertRows(dataStoreId, project1.id, [{ name: 'Alice' }]);
|
||||||
expect(ids).toEqual([1]);
|
expect(ids).toEqual([{ id: 1 }]);
|
||||||
|
|
||||||
// ACT - Try to delete existing and non-existing IDs
|
// ACT - Try to delete existing and non-existing IDs
|
||||||
const result = await dataStoreService.deleteRows(dataStoreId, project1.id, [1, 999, 1000]);
|
const result = await dataStoreService.deleteRows(dataStoreId, project1.id, [1, 999, 1000]);
|
||||||
@@ -1757,7 +1817,7 @@ describe('dataStore', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const ids = await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
const ids = await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
||||||
expect(ids).toEqual([1, 2, 3]);
|
expect(ids).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {});
|
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {});
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ import {
|
|||||||
Workflow,
|
Workflow,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { DataStoreService } from './data-store.service';
|
|
||||||
|
|
||||||
import { OwnershipService } from '@/services/ownership.service';
|
import { OwnershipService } from '@/services/ownership.service';
|
||||||
|
|
||||||
|
import { DataStoreService } from './data-store.service';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class DataStoreProxyService implements DataStoreProxyProvider {
|
export class DataStoreProxyService implements DataStoreProxyProvider {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -6,8 +6,13 @@ import type {
|
|||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import { CreateTable, DslColumn } from '@n8n/db';
|
import { CreateTable, DslColumn } from '@n8n/db';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import { DataSource, DataSourceOptions, QueryRunner, SelectQueryBuilder } from '@n8n/typeorm';
|
import { DataSource, DataSourceOptions, QueryRunner, SelectQueryBuilder, In } from '@n8n/typeorm';
|
||||||
import { DataStoreColumnJsType, DataStoreRows } from 'n8n-workflow';
|
import {
|
||||||
|
DataStoreColumnJsType,
|
||||||
|
DataStoreRows,
|
||||||
|
DataStoreRowWithId,
|
||||||
|
UnexpectedError,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { DataStoreColumn } from './data-store-column.entity';
|
import { DataStoreColumn } from './data-store-column.entity';
|
||||||
import { DataStoreUserTableName } from './data-store.types';
|
import { DataStoreUserTableName } from './data-store.types';
|
||||||
@@ -16,6 +21,7 @@ import {
|
|||||||
buildColumnTypeMap,
|
buildColumnTypeMap,
|
||||||
deleteColumnQuery,
|
deleteColumnQuery,
|
||||||
extractInsertedIds,
|
extractInsertedIds,
|
||||||
|
extractReturningData,
|
||||||
getPlaceholder,
|
getPlaceholder,
|
||||||
normalizeValue,
|
normalizeValue,
|
||||||
quoteIdentifier,
|
quoteIdentifier,
|
||||||
@@ -54,16 +60,26 @@ export class DataStoreRowsRepository {
|
|||||||
return `${tablePrefix}data_store_user_${dataStoreId}`;
|
return `${tablePrefix}data_store_user_${dataStoreId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async insertRows(dataStoreId: string, rows: DataStoreRows, columns: DataStoreColumn[]) {
|
async insertRows(
|
||||||
const insertedIds: number[] = [];
|
dataStoreId: string,
|
||||||
|
rows: DataStoreRows,
|
||||||
|
columns: DataStoreColumn[],
|
||||||
|
returnData: boolean = false,
|
||||||
|
) {
|
||||||
|
const inserted: DataStoreRowWithId[] = [];
|
||||||
|
const dbType = this.dataSource.options.type;
|
||||||
|
const useReturning = dbType === 'postgres' || dbType === 'mariadb';
|
||||||
|
|
||||||
|
const table = this.toTableName(dataStoreId);
|
||||||
|
const columnNames = columns.map((c) => c.name);
|
||||||
|
const escapedColumns = columns.map((c) => this.dataSource.driver.escape(c.name));
|
||||||
|
const selectColumns = ['id', ...escapedColumns];
|
||||||
|
|
||||||
// We insert one by one as the default behavior of returning the last inserted ID
|
// We insert one by one as the default behavior of returning the last inserted ID
|
||||||
// is consistent, whereas getting all inserted IDs when inserting multiple values is
|
// is consistent, whereas getting all inserted IDs when inserting multiple values is
|
||||||
// surprisingly awkward without Entities, e.g. `RETURNING id` explicitly does not aggregate
|
// surprisingly awkward without Entities, e.g. `RETURNING id` explicitly does not aggregate
|
||||||
// and the `identifiers` array output of `execute()` is empty
|
// and the `identifiers` array output of `execute()` is empty
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const dbType = this.dataSource.options.type;
|
|
||||||
|
|
||||||
for (const column of columns) {
|
for (const column of columns) {
|
||||||
row[column.name] = normalizeValue(row[column.name], column.type, dbType);
|
row[column.name] = normalizeValue(row[column.name], column.type, dbType);
|
||||||
}
|
}
|
||||||
@@ -71,22 +87,47 @@ export class DataStoreRowsRepository {
|
|||||||
const query = this.dataSource
|
const query = this.dataSource
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.insert()
|
.insert()
|
||||||
.into(
|
.into(table, columnNames)
|
||||||
this.toTableName(dataStoreId),
|
|
||||||
columns.map((c) => c.name),
|
|
||||||
)
|
|
||||||
.values(row);
|
.values(row);
|
||||||
|
|
||||||
if (dbType === 'postgres' || dbType === 'mariadb') {
|
if (useReturning) {
|
||||||
query.returning('id');
|
query.returning(returnData ? selectColumns.join(',') : 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await query.execute();
|
const result = await query.execute();
|
||||||
|
|
||||||
insertedIds.push(...extractInsertedIds(result.raw, dbType));
|
if (useReturning) {
|
||||||
|
const returned = extractReturningData(result.raw);
|
||||||
|
inserted.push.apply(inserted, returned);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Engines without RETURNING support
|
||||||
|
const rowIds = extractInsertedIds(result.raw, dbType);
|
||||||
|
if (rowIds.length === 0) {
|
||||||
|
throw new UnexpectedError("Couldn't find the inserted row ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!returnData) {
|
||||||
|
inserted.push(...rowIds.map((id) => ({ id })));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertedRow = await this.dataSource
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select(selectColumns)
|
||||||
|
.from(table, 'dataStore')
|
||||||
|
.where({ id: In(rowIds) })
|
||||||
|
.getRawOne<DataStoreRowWithId>();
|
||||||
|
|
||||||
|
if (!insertedRow) {
|
||||||
|
throw new UnexpectedError("Couldn't find the inserted row");
|
||||||
|
}
|
||||||
|
|
||||||
|
inserted.push(insertedRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
return insertedIds;
|
return inserted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TypeORM cannot infer the columns for a dynamic table name, so we use a raw query
|
// TypeORM cannot infer the columns for a dynamic table name, so we use a raw query
|
||||||
|
|||||||
@@ -246,7 +246,12 @@ export class DataStoreController {
|
|||||||
@Body dto: AddDataStoreRowsDto,
|
@Body dto: AddDataStoreRowsDto,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
return await this.dataStoreService.insertRows(dataStoreId, req.params.projectId, dto.data);
|
return await this.dataStoreService.insertRows(
|
||||||
|
dataStoreId,
|
||||||
|
req.params.projectId,
|
||||||
|
dto.data,
|
||||||
|
dto.returnData,
|
||||||
|
);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof DataStoreNotFoundError) {
|
if (e instanceof DataStoreNotFoundError) {
|
||||||
throw new NotFoundError(e.message);
|
throw new NotFoundError(e.message);
|
||||||
|
|||||||
@@ -125,12 +125,17 @@ export class DataStoreService {
|
|||||||
return await this.dataStoreColumnRepository.getColumns(dataStoreId);
|
return await this.dataStoreColumnRepository.getColumns(dataStoreId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async insertRows(dataStoreId: string, projectId: string, rows: DataStoreRows) {
|
async insertRows(
|
||||||
|
dataStoreId: string,
|
||||||
|
projectId: string,
|
||||||
|
rows: DataStoreRows,
|
||||||
|
returnData: boolean = false,
|
||||||
|
) {
|
||||||
await this.validateDataStoreExists(dataStoreId, projectId);
|
await this.validateDataStoreExists(dataStoreId, projectId);
|
||||||
await this.validateRows(dataStoreId, rows);
|
await this.validateRows(dataStoreId, rows);
|
||||||
|
|
||||||
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId);
|
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId);
|
||||||
return await this.dataStoreRowsRepository.insertRows(dataStoreId, rows, columns);
|
return await this.dataStoreRowsRepository.insertRows(dataStoreId, rows, columns, returnData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsertRows(dataStoreId: string, projectId: string, dto: UpsertDataStoreRowsDto) {
|
async upsertRows(dataStoreId: string, projectId: string, dto: UpsertDataStoreRowsDto) {
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import {
|
|||||||
} from '@n8n/api-types';
|
} from '@n8n/api-types';
|
||||||
import { DslColumn } from '@n8n/db';
|
import { DslColumn } from '@n8n/db';
|
||||||
import type { DataSourceOptions } from '@n8n/typeorm';
|
import type { DataSourceOptions } from '@n8n/typeorm';
|
||||||
import type { DataStoreColumnJsType, DataStoreRows } from 'n8n-workflow';
|
import type { DataStoreColumnJsType, DataStoreRows, DataStoreRowWithId } from 'n8n-workflow';
|
||||||
import { UnexpectedError } from 'n8n-workflow';
|
import { UnexpectedError } from 'n8n-workflow';
|
||||||
|
|
||||||
import type { DataStoreUserTableName } from '../data-store.types';
|
|
||||||
|
|
||||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
|
|
||||||
|
import type { DataStoreUserTableName } from '../data-store.types';
|
||||||
|
|
||||||
export function toDslColumns(columns: DataStoreCreateColumnSchema[]): DslColumn[] {
|
export function toDslColumns(columns: DataStoreCreateColumnSchema[]): DslColumn[] {
|
||||||
return columns.map((col) => {
|
return columns.map((col) => {
|
||||||
const name = new DslColumn(col.name.trim());
|
const name = new DslColumn(col.name.trim());
|
||||||
@@ -155,6 +155,16 @@ function hasRowId(data: unknown): data is WithRowId {
|
|||||||
return typeof data === 'object' && data !== null && 'id' in data && isNumber(data.id);
|
return typeof data === 'object' && data !== null && 'id' in data && isNumber(data.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractReturningData(raw: unknown): DataStoreRowWithId[] {
|
||||||
|
if (!isArrayOf(raw, hasRowId)) {
|
||||||
|
throw new UnexpectedError(
|
||||||
|
'Expected INSERT INTO raw to be { id: number }[] on Postgres or MariaDB',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
export function extractInsertedIds(raw: unknown, dbType: DataSourceOptions['type']): number[] {
|
export function extractInsertedIds(raw: unknown, dbType: DataSourceOptions['type']): number[] {
|
||||||
switch (dbType) {
|
switch (dbType) {
|
||||||
case 'postgres':
|
case 'postgres':
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export type DataStoreColumnJsType = string | number | boolean | Date;
|
|||||||
|
|
||||||
export type DataStoreRow = Record<string, DataStoreColumnJsType | null>;
|
export type DataStoreRow = Record<string, DataStoreColumnJsType | null>;
|
||||||
export type DataStoreRows = DataStoreRow[];
|
export type DataStoreRows = DataStoreRow[];
|
||||||
|
export type DataStoreRowWithId = DataStoreRow & { id: number };
|
||||||
|
|
||||||
// APIs for a data store service operating on a specific projectId
|
// APIs for a data store service operating on a specific projectId
|
||||||
export interface IDataStoreProjectAggregateService {
|
export interface IDataStoreProjectAggregateService {
|
||||||
@@ -101,7 +102,7 @@ export interface IDataStoreProjectService {
|
|||||||
dto: Partial<ListDataStoreRowsOptions>,
|
dto: Partial<ListDataStoreRowsOptions>,
|
||||||
): Promise<{ count: number; data: DataStoreRows }>;
|
): Promise<{ count: number; data: DataStoreRows }>;
|
||||||
|
|
||||||
insertRows(rows: DataStoreRows): Promise<number[]>;
|
insertRows(rows: DataStoreRows): Promise<Array<{ id: number }>>;
|
||||||
|
|
||||||
upsertRows(options: UpsertDataStoreRowsOptions): Promise<boolean>;
|
upsertRows(options: UpsertDataStoreRowsOptions): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user