@fachkraftfreund/n8n-nodes-supabase 1.3.14 → 1.4.1
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.
|
@@ -565,7 +565,7 @@ class Supabase {
|
|
|
565
565
|
displayOptions: {
|
|
566
566
|
show: {
|
|
567
567
|
resource: ['database'],
|
|
568
|
-
operation: ['read'],
|
|
568
|
+
operation: ['read', 'count'],
|
|
569
569
|
},
|
|
570
570
|
},
|
|
571
571
|
options: [
|
|
@@ -729,6 +729,37 @@ class Supabase {
|
|
|
729
729
|
},
|
|
730
730
|
},
|
|
731
731
|
},
|
|
732
|
+
{
|
|
733
|
+
displayName: 'Batch Counts',
|
|
734
|
+
name: 'batchCounts',
|
|
735
|
+
type: 'boolean',
|
|
736
|
+
default: false,
|
|
737
|
+
description: 'Whether to combine all input items into a single GROUP BY query instead of one count per item. Requires the exec_sql_select RPC function.',
|
|
738
|
+
displayOptions: {
|
|
739
|
+
show: {
|
|
740
|
+
resource: ['database'],
|
|
741
|
+
operation: ['count'],
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
displayName: 'Group By Column',
|
|
747
|
+
name: 'groupByColumn',
|
|
748
|
+
type: 'options',
|
|
749
|
+
typeOptions: {
|
|
750
|
+
loadOptionsMethod: 'getColumns',
|
|
751
|
+
},
|
|
752
|
+
required: true,
|
|
753
|
+
default: '',
|
|
754
|
+
description: 'Column to group counts by (e.g. sector). Each unique value produces one output item with its count.',
|
|
755
|
+
displayOptions: {
|
|
756
|
+
show: {
|
|
757
|
+
resource: ['database'],
|
|
758
|
+
operation: ['count'],
|
|
759
|
+
batchCounts: [true],
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
},
|
|
732
763
|
{
|
|
733
764
|
displayName: 'Bucket',
|
|
734
765
|
name: 'bucket',
|
|
@@ -1023,7 +1054,23 @@ class Supabase {
|
|
|
1023
1054
|
const supabase = (0, supabaseClient_1.createSupabaseClient)(credentials);
|
|
1024
1055
|
const resource = this.getNodeParameter('resource', 0);
|
|
1025
1056
|
const operation = this.getNodeParameter('operation', 0);
|
|
1026
|
-
if (resource === 'database' &&
|
|
1057
|
+
if (resource === 'database' && operation === 'count' && this.getNodeParameter('batchCounts', 0, false)) {
|
|
1058
|
+
try {
|
|
1059
|
+
const results = await database_1.executeBulkDatabaseOperation.call(this, supabase, operation, items.length);
|
|
1060
|
+
for (const r of results)
|
|
1061
|
+
returnData.push(r);
|
|
1062
|
+
}
|
|
1063
|
+
catch (error) {
|
|
1064
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1065
|
+
if (this.continueOnFail()) {
|
|
1066
|
+
returnData.push({ json: { error: errorMessage } });
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), errorMessage);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
else if (resource === 'database' && ['create', 'upsert', 'update'].includes(operation)) {
|
|
1027
1074
|
const firstItem = items[0];
|
|
1028
1075
|
if (items.length === 1 &&
|
|
1029
1076
|
(firstItem === null || firstItem === void 0 ? void 0 : firstItem.json) &&
|
|
@@ -1045,7 +1092,7 @@ class Supabase {
|
|
|
1045
1092
|
}
|
|
1046
1093
|
}
|
|
1047
1094
|
}
|
|
1048
|
-
else if (resource === 'database' &&
|
|
1095
|
+
else if (resource === 'database' && operation === 'read') {
|
|
1049
1096
|
try {
|
|
1050
1097
|
const operationResults = await database_1.executeDatabaseOperation.call(this, supabase, operation, 0, credentials.host);
|
|
1051
1098
|
for (const r of operationResults)
|
|
@@ -178,6 +178,8 @@ async function executeBulkDatabaseOperation(supabase, operation, itemCount) {
|
|
|
178
178
|
return await handleBulkUpsert.call(this, supabase, itemCount);
|
|
179
179
|
case 'update':
|
|
180
180
|
return await handleBulkUpdate.call(this, supabase, itemCount);
|
|
181
|
+
case 'count':
|
|
182
|
+
return await handleBatchCount.call(this, supabase, itemCount);
|
|
181
183
|
default:
|
|
182
184
|
throw new Error(`Operation ${operation} does not support bulk mode`);
|
|
183
185
|
}
|
|
@@ -786,12 +788,22 @@ async function handleCount(supabase, itemIndex, hostUrl) {
|
|
|
786
788
|
const table = this.getNodeParameter('table', itemIndex);
|
|
787
789
|
(0, supabaseClient_1.validateTableName)(table);
|
|
788
790
|
const filters = getFilters(this, itemIndex);
|
|
789
|
-
const
|
|
791
|
+
const joins = this.getNodeParameter('joins.join', itemIndex, []);
|
|
792
|
+
let selectWithJoins = '*';
|
|
793
|
+
for (const j of joins) {
|
|
794
|
+
if (!j.table)
|
|
795
|
+
continue;
|
|
796
|
+
const cols = j.columns || '*';
|
|
797
|
+
const hint = j.joinType === 'inner' ? `${j.table}!inner` : j.table;
|
|
798
|
+
selectWithJoins += `,${hint}(${cols})`;
|
|
799
|
+
}
|
|
800
|
+
const overhead = (0, supabaseClient_1.estimateUrlOverhead)(hostUrl, table, selectWithJoins, filters);
|
|
790
801
|
const maxInChars = Math.max(500, supabaseClient_1.MAX_SAFE_URL_LENGTH - overhead);
|
|
791
|
-
const
|
|
802
|
+
const maxItems = (0, supabaseClient_1.computeMaxIdsPerChunk)(selectWithJoins);
|
|
803
|
+
const filterChunks = (0, supabaseClient_1.expandChunkedFilters)(filters, maxInChars, maxItems);
|
|
792
804
|
let totalCount = 0;
|
|
793
805
|
for (const chunkFilters of filterChunks) {
|
|
794
|
-
let query = supabase.from(table).select(
|
|
806
|
+
let query = supabase.from(table).select(selectWithJoins, { count: 'exact', head: true });
|
|
795
807
|
for (const filter of chunkFilters) {
|
|
796
808
|
const operator = (0, supabaseClient_1.convertFilterOperator)(filter.operator);
|
|
797
809
|
query = query.filter(filter.column, operator, (0, supabaseClient_1.normalizeFilterValue)(filter.operator, filter.value));
|
|
@@ -803,3 +815,90 @@ async function handleCount(supabase, itemIndex, hostUrl) {
|
|
|
803
815
|
}
|
|
804
816
|
return [{ json: { count: totalCount, table } }];
|
|
805
817
|
}
|
|
818
|
+
async function handleBatchCount(supabase, itemCount) {
|
|
819
|
+
const table = this.getNodeParameter('table', 0);
|
|
820
|
+
const groupByColumn = this.getNodeParameter('groupByColumn', 0);
|
|
821
|
+
(0, supabaseClient_1.validateTableName)(table);
|
|
822
|
+
(0, supabaseClient_1.validateColumnName)(groupByColumn);
|
|
823
|
+
const baseFilters = getFilters(this, 0);
|
|
824
|
+
const groupValues = new Set();
|
|
825
|
+
for (let i = 0; i < itemCount; i++) {
|
|
826
|
+
const itemFilters = getFilters(this, i);
|
|
827
|
+
for (const f of itemFilters) {
|
|
828
|
+
if (f.column === groupByColumn) {
|
|
829
|
+
groupValues.add(String(f.value));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
const whereClauses = [];
|
|
834
|
+
const staticFilters = baseFilters.filter(f => f.column !== groupByColumn);
|
|
835
|
+
for (const f of staticFilters) {
|
|
836
|
+
const col = `"${f.column}"`;
|
|
837
|
+
switch (f.operator) {
|
|
838
|
+
case 'eq':
|
|
839
|
+
whereClauses.push(`${col} = '${String(f.value).replace(/'/g, "''")}'`);
|
|
840
|
+
break;
|
|
841
|
+
case 'neq':
|
|
842
|
+
whereClauses.push(`${col} != '${String(f.value).replace(/'/g, "''")}'`);
|
|
843
|
+
break;
|
|
844
|
+
case 'gt':
|
|
845
|
+
whereClauses.push(`${col} > '${String(f.value).replace(/'/g, "''")}'`);
|
|
846
|
+
break;
|
|
847
|
+
case 'gte':
|
|
848
|
+
whereClauses.push(`${col} >= '${String(f.value).replace(/'/g, "''")}'`);
|
|
849
|
+
break;
|
|
850
|
+
case 'lt':
|
|
851
|
+
whereClauses.push(`${col} < '${String(f.value).replace(/'/g, "''")}'`);
|
|
852
|
+
break;
|
|
853
|
+
case 'lte':
|
|
854
|
+
whereClauses.push(`${col} <= '${String(f.value).replace(/'/g, "''")}'`);
|
|
855
|
+
break;
|
|
856
|
+
case 'is':
|
|
857
|
+
whereClauses.push(`${col} IS ${f.value}`);
|
|
858
|
+
break;
|
|
859
|
+
case 'like':
|
|
860
|
+
whereClauses.push(`${col} LIKE '${String(f.value).replace(/'/g, "''")}'`);
|
|
861
|
+
break;
|
|
862
|
+
case 'ilike':
|
|
863
|
+
whereClauses.push(`${col} ILIKE '${String(f.value).replace(/'/g, "''")}'`);
|
|
864
|
+
break;
|
|
865
|
+
case 'in': {
|
|
866
|
+
const vals = Array.isArray(f.value) ? f.value : String(f.value).split(',');
|
|
867
|
+
const escaped = vals.map(v => `'${String(v).trim().replace(/'/g, "''")}'`).join(',');
|
|
868
|
+
whereClauses.push(`${col} IN (${escaped})`);
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
default: whereClauses.push(`${col} = '${String(f.value).replace(/'/g, "''")}'`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (groupValues.size > 0) {
|
|
875
|
+
const escaped = Array.from(groupValues).map(v => `'${v.replace(/'/g, "''")}'`).join(',');
|
|
876
|
+
whereClauses.push(`"${groupByColumn}" IN (${escaped})`);
|
|
877
|
+
}
|
|
878
|
+
const joins = this.getNodeParameter('joins.join', 0, []);
|
|
879
|
+
const joinClauses = [];
|
|
880
|
+
for (const j of joins) {
|
|
881
|
+
if (!j.table)
|
|
882
|
+
continue;
|
|
883
|
+
(0, supabaseClient_1.validateTableName)(j.table);
|
|
884
|
+
const joinType = j.joinType === 'inner' ? 'INNER JOIN' : 'LEFT JOIN';
|
|
885
|
+
joinClauses.push(`${joinType} "${j.table}" ON "${j.table}"."${table.replace(/s$/, '')}_id" = "${table}"."id"`);
|
|
886
|
+
}
|
|
887
|
+
const whereStr = whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '';
|
|
888
|
+
const joinStr = joinClauses.length > 0 ? ` ${joinClauses.join(' ')}` : '';
|
|
889
|
+
const sql = `SELECT "${groupByColumn}", COUNT(*) as count FROM "${table}"${joinStr}${whereStr} GROUP BY "${groupByColumn}" ORDER BY count DESC`;
|
|
890
|
+
console.log(`[Supabase BATCH COUNT] sql: ${sql}`);
|
|
891
|
+
const { data, error } = await supabase.rpc('exec_sql_select', { sql });
|
|
892
|
+
if (error)
|
|
893
|
+
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
894
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
895
|
+
return [{ json: { table, groupByColumn, counts: [], message: 'No rows matched' } }];
|
|
896
|
+
}
|
|
897
|
+
return data.map((row) => ({
|
|
898
|
+
json: {
|
|
899
|
+
[groupByColumn]: row[groupByColumn],
|
|
900
|
+
count: Number(row.count),
|
|
901
|
+
table,
|
|
902
|
+
},
|
|
903
|
+
}));
|
|
904
|
+
}
|
package/package.json
CHANGED