mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-03-08 01:53:30 +01:00
Finished prototype local backends and expression editor
This commit is contained in:
@@ -13,17 +13,17 @@ const QueryBuilder = require('./QueryBuilder');
|
||||
const SchemaManager = require('./SchemaManager');
|
||||
|
||||
/**
|
||||
* Generate a unique object ID (similar to Parse objectId)
|
||||
* Generate a UUID v4
|
||||
*
|
||||
* @returns {string} 10-character alphanumeric ID
|
||||
* @returns {string} UUID string (e.g., "123e4567-e89b-12d3-a456-426614174000")
|
||||
*/
|
||||
function generateObjectId() {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let id = '';
|
||||
for (let i = 0; i < 10; i++) {
|
||||
id += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return id;
|
||||
function generateUUID() {
|
||||
// RFC 4122 version 4 UUID
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,17 +132,25 @@ class LocalSQLAdapter {
|
||||
const self = this;
|
||||
return {
|
||||
ensureSchemaTable: () => {},
|
||||
createTable: ({ name }) => {
|
||||
createTable: ({ name, columns }) => {
|
||||
if (!self._mockData[name]) {
|
||||
self._mockData[name] = {};
|
||||
self._mockSchema[name] = {};
|
||||
self._mockSchema[name] = { name, columns: columns || [] };
|
||||
}
|
||||
return true;
|
||||
},
|
||||
addColumn: (table, col) => {
|
||||
if (!self._mockSchema[table]) self._mockSchema[table] = {};
|
||||
self._mockSchema[table][col.name] = col;
|
||||
if (!self._mockSchema[table]) self._mockSchema[table] = { name: table, columns: [] };
|
||||
if (!self._mockSchema[table].columns) self._mockSchema[table].columns = [];
|
||||
// Check if column already exists
|
||||
const exists = self._mockSchema[table].columns.some((c) => c.name === col.name);
|
||||
if (!exists) {
|
||||
self._mockSchema[table].columns.push(col);
|
||||
}
|
||||
},
|
||||
getTableSchema: (table) => self._mockSchema[table] || {},
|
||||
getTableSchema: (table) => self._mockSchema[table] || null,
|
||||
listTables: () => Object.keys(self._mockData).filter((name) => !name.startsWith('_')),
|
||||
exportSchemas: () => Object.values(self._mockSchema).filter((s) => s && !s.name?.startsWith('_')),
|
||||
addRelation: () => {},
|
||||
removeRelation: () => {}
|
||||
};
|
||||
@@ -153,8 +161,9 @@ class LocalSQLAdapter {
|
||||
* @private
|
||||
*/
|
||||
_mockExec(sql, params, mode) {
|
||||
// Parse simple SQL patterns for mock execution
|
||||
const selectMatch = sql.match(/SELECT \* FROM "?(\w+)"?\s*(?:WHERE "?objectId"?\s*=\s*\?)?/i);
|
||||
// Parse SQL patterns for mock execution
|
||||
// Match SELECT with optional WHERE, ORDER BY, LIMIT, OFFSET
|
||||
const selectMatch = sql.match(/SELECT\s+\*\s+FROM\s+"?(\w+)"?/i);
|
||||
const insertMatch = sql.match(/INSERT INTO "?(\w+)"?/i);
|
||||
const updateMatch = sql.match(/UPDATE "?(\w+)"?\s+SET/i);
|
||||
const deleteMatch = sql.match(/DELETE FROM "?(\w+)"?/i);
|
||||
@@ -163,48 +172,111 @@ class LocalSQLAdapter {
|
||||
const table = selectMatch[1];
|
||||
if (!this._mockData[table]) this._mockData[table] = {};
|
||||
|
||||
if (params.length > 0) {
|
||||
// Single record fetch
|
||||
const record = this._mockData[table][params[0]];
|
||||
let records = Object.values(this._mockData[table]);
|
||||
|
||||
// Check for WHERE id = ? or WHERE objectId = ?
|
||||
const idMatch = sql.match(/WHERE\s+"?(?:id|objectId)"?\s*=\s*\?/i);
|
||||
if (idMatch && params.length > 0) {
|
||||
const recordId = params[0];
|
||||
const record = this._mockData[table][recordId];
|
||||
return mode === 'get' ? record || null : record ? [record] : [];
|
||||
}
|
||||
// Return all records
|
||||
const records = Object.values(this._mockData[table]);
|
||||
|
||||
// Handle ORDER BY
|
||||
const orderMatch = sql.match(/ORDER BY\s+"?(\w+)"?\s+(ASC|DESC)?/i);
|
||||
if (orderMatch) {
|
||||
const orderCol = orderMatch[1];
|
||||
const orderDir = (orderMatch[2] || 'ASC').toUpperCase();
|
||||
records = records.sort((a, b) => {
|
||||
const aVal = a[orderCol];
|
||||
const bVal = b[orderCol];
|
||||
if (aVal < bVal) return orderDir === 'ASC' ? -1 : 1;
|
||||
if (aVal > bVal) return orderDir === 'ASC' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Handle LIMIT and OFFSET
|
||||
// Find position of LIMIT in params (it comes after WHERE params if any)
|
||||
let paramIndex = 0;
|
||||
if (sql.includes('LIMIT')) {
|
||||
// Find LIMIT param position - it's after WHERE params
|
||||
const limitIdx = sql.indexOf('LIMIT');
|
||||
const whereClause = sql.substring(0, limitIdx);
|
||||
const whereParams = (whereClause.match(/\?/g) || []).length;
|
||||
paramIndex = whereParams;
|
||||
|
||||
const limit = params[paramIndex];
|
||||
const skip = sql.includes('OFFSET') ? params[paramIndex + 1] || 0 : 0;
|
||||
records = records.slice(skip, skip + limit);
|
||||
}
|
||||
|
||||
return mode === 'get' ? records[0] || null : records;
|
||||
}
|
||||
|
||||
if (insertMatch) {
|
||||
const table = insertMatch[1];
|
||||
if (!this._mockData[table]) this._mockData[table] = {};
|
||||
// Find objectId in params (simple extraction)
|
||||
|
||||
// Parse column names from SQL: INSERT INTO table (col1, col2, ...) VALUES (?, ?, ...)
|
||||
const columnsMatch = sql.match(/\(([^)]+)\)\s*VALUES/i);
|
||||
if (columnsMatch) {
|
||||
const columns = columnsMatch[1].split(',').map((c) => c.trim().replace(/"/g, ''));
|
||||
const record = {};
|
||||
columns.forEach((col, idx) => {
|
||||
record[col] = params[idx];
|
||||
});
|
||||
// Ensure id exists
|
||||
if (!record.id && params[0]) {
|
||||
record.id = params[0];
|
||||
}
|
||||
const recordId = record.id;
|
||||
this._mockData[table][recordId] = record;
|
||||
return { changes: 1 };
|
||||
}
|
||||
|
||||
// Fallback: simple record creation
|
||||
const now = new Date().toISOString();
|
||||
const record = { objectId: params[0], createdAt: now, updatedAt: now };
|
||||
const record = { id: params[0], createdAt: now, updatedAt: now };
|
||||
this._mockData[table][params[0]] = record;
|
||||
return { changes: 1 };
|
||||
}
|
||||
|
||||
if (updateMatch) {
|
||||
const table = updateMatch[1];
|
||||
// Last param is typically the objectId in WHERE clause
|
||||
const objectId = params[params.length - 1];
|
||||
if (this._mockData[table] && this._mockData[table][objectId]) {
|
||||
this._mockData[table][objectId].updatedAt = new Date().toISOString();
|
||||
// Last param is typically the id in WHERE clause
|
||||
const recordId = params[params.length - 1];
|
||||
if (this._mockData[table] && this._mockData[table][recordId]) {
|
||||
// Parse SET clauses to update actual fields
|
||||
const setMatch = sql.match(/SET\s+(.+?)\s+WHERE/i);
|
||||
if (setMatch) {
|
||||
const setParts = setMatch[1].split(',');
|
||||
let paramIdx = 0;
|
||||
setParts.forEach((part) => {
|
||||
const colMatch = part.match(/"?(\w+)"?\s*=/);
|
||||
if (colMatch) {
|
||||
this._mockData[table][recordId][colMatch[1]] = params[paramIdx];
|
||||
paramIdx++;
|
||||
}
|
||||
});
|
||||
}
|
||||
this._mockData[table][recordId].updatedAt = new Date().toISOString();
|
||||
}
|
||||
return { changes: 1 };
|
||||
}
|
||||
|
||||
if (deleteMatch) {
|
||||
const table = deleteMatch[1];
|
||||
const objectId = params[0];
|
||||
const recordId = params[0];
|
||||
if (this._mockData[table]) {
|
||||
delete this._mockData[table][objectId];
|
||||
delete this._mockData[table][recordId];
|
||||
}
|
||||
return { changes: 1 };
|
||||
}
|
||||
|
||||
// Count query
|
||||
if (sql.includes('COUNT(*)')) {
|
||||
const countMatch = sql.match(/FROM "?(\w+)"?/i);
|
||||
const countMatch = sql.match(/FROM\s+"?(\w+)"?/i);
|
||||
if (countMatch) {
|
||||
const table = countMatch[1];
|
||||
const count = Object.keys(this._mockData[table] || {}).length;
|
||||
@@ -356,8 +428,9 @@ class LocalSQLAdapter {
|
||||
try {
|
||||
this._ensureTable(options.collection);
|
||||
|
||||
const sql = `SELECT * FROM ${QueryBuilder.escapeTable(options.collection)} WHERE "objectId" = ?`;
|
||||
const row = this.db.prepare(sql).get(options.objectId);
|
||||
const sql = `SELECT * FROM ${QueryBuilder.escapeTable(options.collection)} WHERE "id" = ?`;
|
||||
const recordId = options.id || options.objectId;
|
||||
const row = this.db.prepare(sql).get(recordId);
|
||||
|
||||
if (!row) {
|
||||
options.error('Object not found');
|
||||
@@ -370,7 +443,7 @@ class LocalSQLAdapter {
|
||||
|
||||
this.events.emit('fetch', {
|
||||
type: 'fetch',
|
||||
objectId: options.objectId,
|
||||
id: recordId,
|
||||
object: record,
|
||||
collection: options.collection
|
||||
});
|
||||
@@ -392,22 +465,22 @@ class LocalSQLAdapter {
|
||||
// Auto-add columns for new fields
|
||||
if (this.options.autoCreateTables && this.schemaManager) {
|
||||
for (const [key, value] of Object.entries(options.data)) {
|
||||
if (key !== 'objectId' && key !== 'createdAt' && key !== 'updatedAt') {
|
||||
if (key !== 'id' && key !== 'createdAt' && key !== 'updatedAt') {
|
||||
const type = this._inferType(value);
|
||||
this.schemaManager.addColumn(options.collection, { name: key, type });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const objectId = generateObjectId();
|
||||
const { sql, params } = QueryBuilder.buildInsert(options, objectId);
|
||||
const recordId = generateUUID();
|
||||
const { sql, params } = QueryBuilder.buildInsert(options, recordId);
|
||||
|
||||
this.db.prepare(sql).run(...params);
|
||||
|
||||
// Fetch the created record to get all fields
|
||||
const createdRow = this.db
|
||||
.prepare(`SELECT * FROM ${QueryBuilder.escapeTable(options.collection)} WHERE "objectId" = ?`)
|
||||
.get(objectId);
|
||||
.prepare(`SELECT * FROM ${QueryBuilder.escapeTable(options.collection)} WHERE "id" = ?`)
|
||||
.get(recordId);
|
||||
|
||||
const record = this._rowToRecord(createdRow, options.collection);
|
||||
|
||||
@@ -415,7 +488,7 @@ class LocalSQLAdapter {
|
||||
|
||||
this.events.emit('create', {
|
||||
type: 'create',
|
||||
objectId,
|
||||
id: recordId,
|
||||
object: record,
|
||||
collection: options.collection
|
||||
});
|
||||
@@ -437,20 +510,21 @@ class LocalSQLAdapter {
|
||||
// Auto-add columns for new fields
|
||||
if (this.options.autoCreateTables && this.schemaManager) {
|
||||
for (const [key, value] of Object.entries(options.data)) {
|
||||
if (key !== 'objectId' && key !== 'createdAt' && key !== 'updatedAt') {
|
||||
if (key !== 'id' && key !== 'createdAt' && key !== 'updatedAt') {
|
||||
const type = this._inferType(value);
|
||||
this.schemaManager.addColumn(options.collection, { name: key, type });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const recordId = options.id || options.objectId;
|
||||
const { sql, params } = QueryBuilder.buildUpdate(options);
|
||||
this.db.prepare(sql).run(...params);
|
||||
|
||||
// Fetch the updated record
|
||||
const updatedRow = this.db
|
||||
.prepare(`SELECT * FROM ${QueryBuilder.escapeTable(options.collection)} WHERE "objectId" = ?`)
|
||||
.get(options.objectId);
|
||||
.prepare(`SELECT * FROM ${QueryBuilder.escapeTable(options.collection)} WHERE "id" = ?`)
|
||||
.get(recordId);
|
||||
|
||||
const record = this._rowToRecord(updatedRow, options.collection);
|
||||
|
||||
@@ -458,7 +532,7 @@ class LocalSQLAdapter {
|
||||
|
||||
this.events.emit('save', {
|
||||
type: 'save',
|
||||
objectId: options.objectId,
|
||||
id: recordId,
|
||||
object: record,
|
||||
collection: options.collection
|
||||
});
|
||||
@@ -482,9 +556,10 @@ class LocalSQLAdapter {
|
||||
|
||||
options.success();
|
||||
|
||||
const recordId = options.id || options.objectId;
|
||||
this.events.emit('delete', {
|
||||
type: 'delete',
|
||||
objectId: options.objectId,
|
||||
id: recordId,
|
||||
collection: options.collection
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -573,9 +648,10 @@ class LocalSQLAdapter {
|
||||
this.db.prepare(sql).run(...params);
|
||||
|
||||
// Fetch the updated record
|
||||
const recordId = options.id || options.objectId;
|
||||
const updatedRow = this.db
|
||||
.prepare(`SELECT * FROM ${QueryBuilder.escapeTable(options.collection)} WHERE "objectId" = ?`)
|
||||
.get(options.objectId);
|
||||
.prepare(`SELECT * FROM ${QueryBuilder.escapeTable(options.collection)} WHERE "id" = ?`)
|
||||
.get(recordId);
|
||||
|
||||
const record = this._rowToRecord(updatedRow, options.collection);
|
||||
options.success(record);
|
||||
|
||||
@@ -275,6 +275,12 @@ function translateOperator(col, op, value, params, schema) {
|
||||
}
|
||||
return null;
|
||||
|
||||
case 'contains':
|
||||
case '$contains':
|
||||
// Contains search - convert to LIKE with wildcards
|
||||
params.push(`%${convertedValue}%`);
|
||||
return `${col} LIKE ?`;
|
||||
|
||||
// Geo queries - not fully supported in SQLite without extensions
|
||||
case '$nearSphere':
|
||||
case '$within':
|
||||
@@ -333,8 +339,8 @@ function buildSelect(options, schema) {
|
||||
let selectClause = '*';
|
||||
if (options.select) {
|
||||
const selectArray = Array.isArray(options.select) ? options.select : options.select.split(',');
|
||||
// Always include objectId
|
||||
const fields = new Set(['objectId', ...selectArray.map((s) => s.trim())]);
|
||||
// Always include id
|
||||
const fields = new Set(['id', ...selectArray.map((s) => s.trim())]);
|
||||
selectClause = Array.from(fields)
|
||||
.map((f) => escapeColumn(f))
|
||||
.join(', ');
|
||||
@@ -406,13 +412,13 @@ function buildCount(options, schema) {
|
||||
* @param {string} objectId
|
||||
* @returns {{ sql: string, params: Array }}
|
||||
*/
|
||||
function buildInsert(options, objectId) {
|
||||
function buildInsert(options, id) {
|
||||
const params = [];
|
||||
const table = escapeTable(options.collection);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const data = {
|
||||
objectId,
|
||||
id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
...options.data
|
||||
@@ -441,7 +447,7 @@ function buildInsert(options, objectId) {
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {string} options.collection
|
||||
* @param {string} options.objectId
|
||||
* @param {string} options.id - Record ID
|
||||
* @param {Object} options.data
|
||||
* @returns {{ sql: string, params: Array }}
|
||||
*/
|
||||
@@ -455,7 +461,7 @@ function buildUpdate(options) {
|
||||
data.updatedAt = new Date().toISOString();
|
||||
|
||||
// Remove protected fields
|
||||
delete data.objectId;
|
||||
delete data.id;
|
||||
delete data.createdAt;
|
||||
delete data._createdAt;
|
||||
delete data._updatedAt;
|
||||
@@ -467,9 +473,11 @@ function buildUpdate(options) {
|
||||
params.push(serializeValue(value));
|
||||
}
|
||||
|
||||
params.push(options.objectId);
|
||||
// Use id or objectId for backwards compatibility
|
||||
const recordId = options.id || options.objectId;
|
||||
params.push(recordId);
|
||||
|
||||
const sql = `UPDATE ${table} SET ${setClause.join(', ')} WHERE "objectId" = ?`;
|
||||
const sql = `UPDATE ${table} SET ${setClause.join(', ')} WHERE "id" = ?`;
|
||||
|
||||
return { sql, params };
|
||||
}
|
||||
@@ -479,13 +487,15 @@ function buildUpdate(options) {
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {string} options.collection
|
||||
* @param {string} options.objectId
|
||||
* @param {string} options.id - Record ID
|
||||
* @returns {{ sql: string, params: Array }}
|
||||
*/
|
||||
function buildDelete(options) {
|
||||
const table = escapeTable(options.collection);
|
||||
const sql = `DELETE FROM ${table} WHERE "objectId" = ?`;
|
||||
return { sql, params: [options.objectId] };
|
||||
// Use id or objectId for backwards compatibility
|
||||
const recordId = options.id || options.objectId;
|
||||
const sql = `DELETE FROM ${table} WHERE "id" = ?`;
|
||||
return { sql, params: [recordId] };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -493,7 +503,7 @@ function buildDelete(options) {
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {string} options.collection
|
||||
* @param {string} options.objectId
|
||||
* @param {string} options.id - Record ID
|
||||
* @param {Object<string, number>} options.properties
|
||||
* @returns {{ sql: string, params: Array }}
|
||||
*/
|
||||
@@ -513,9 +523,11 @@ function buildIncrement(options) {
|
||||
setClause.push('"updatedAt" = ?');
|
||||
params.push(new Date().toISOString());
|
||||
|
||||
params.push(options.objectId);
|
||||
// Use id or objectId for backwards compatibility
|
||||
const recordId = options.id || options.objectId;
|
||||
params.push(recordId);
|
||||
|
||||
const sql = `UPDATE ${table} SET ${setClause.join(', ')} WHERE "objectId" = ?`;
|
||||
const sql = `UPDATE ${table} SET ${setClause.join(', ')} WHERE "id" = ?`;
|
||||
|
||||
return { sql, params };
|
||||
}
|
||||
|
||||
@@ -168,6 +168,89 @@ class SchemaManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a table and all its data
|
||||
*
|
||||
* @param {string} tableName - Table name
|
||||
* @returns {boolean} Whether table was deleted
|
||||
*/
|
||||
deleteTable(tableName) {
|
||||
// Check if table exists
|
||||
const exists = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(tableName);
|
||||
|
||||
if (!exists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Drop the table
|
||||
this.db.exec(`DROP TABLE IF EXISTS ${escapeTable(tableName)}`);
|
||||
|
||||
// Remove from schema tracking
|
||||
this.ensureSchemaTable();
|
||||
this.db.prepare('DELETE FROM "_Schema" WHERE "name" = ?').run(tableName);
|
||||
|
||||
// Clear cache
|
||||
this._schemaCache.delete(tableName);
|
||||
|
||||
// Drop any junction tables for relations
|
||||
const junctionTables = this.db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE ?")
|
||||
.all(`_Join_%_${tableName}`);
|
||||
|
||||
for (const jt of junctionTables) {
|
||||
this.db.exec(`DROP TABLE IF EXISTS ${escapeTable(jt.name)}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a column in a table (SQLite 3.25.0+)
|
||||
*
|
||||
* @param {string} tableName - Table name
|
||||
* @param {string} oldName - Current column name
|
||||
* @param {string} newName - New column name
|
||||
* @returns {boolean} Whether column was renamed
|
||||
*/
|
||||
renameColumn(tableName, oldName, newName) {
|
||||
// Validate new name
|
||||
if (!newName || !/^[a-zA-Z][a-zA-Z0-9_]*$/.test(newName)) {
|
||||
throw new Error('Invalid column name');
|
||||
}
|
||||
|
||||
// Can't rename system columns
|
||||
const systemCols = ['objectId', 'createdAt', 'updatedAt', 'ACL'];
|
||||
if (systemCols.includes(oldName)) {
|
||||
throw new Error('Cannot rename system columns');
|
||||
}
|
||||
|
||||
try {
|
||||
this.db.exec(
|
||||
`ALTER TABLE ${escapeTable(tableName)} RENAME COLUMN ${escapeColumn(oldName)} TO ${escapeColumn(newName)}`
|
||||
);
|
||||
|
||||
// Update schema tracking
|
||||
const schema = this.getTableSchema(tableName);
|
||||
if (schema && schema.columns) {
|
||||
const col = schema.columns.find((c) => c.name === oldName);
|
||||
if (col) {
|
||||
col.name = newName;
|
||||
this.db
|
||||
.prepare(`UPDATE "_Schema" SET "schema" = ?, "updatedAt" = CURRENT_TIMESTAMP WHERE "name" = ?`)
|
||||
.run(JSON.stringify(schema), tableName);
|
||||
this._schemaCache.set(tableName, schema);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e.message.includes('no such column')) {
|
||||
throw new Error(`Column "${oldName}" does not exist`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schema for a table
|
||||
*
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
const OutputProperty = require('./outputproperty');
|
||||
const { evaluateExpression } = require('./expression-evaluator');
|
||||
const {
|
||||
evaluateExpression,
|
||||
compileExpression,
|
||||
detectDependencies,
|
||||
subscribeToChanges
|
||||
} = require('./expression-evaluator');
|
||||
const { coerceToType } = require('./expression-type-coercion');
|
||||
|
||||
/**
|
||||
@@ -45,6 +50,9 @@ function Node(context, id) {
|
||||
|
||||
this._valuesFromConnections = {};
|
||||
this.updateOnDirtyFlagging = true;
|
||||
|
||||
// Expression subscriptions: { [portName]: { unsub: unsubscribeFn, expression: string } }
|
||||
this._expressionSubscriptions = {};
|
||||
}
|
||||
|
||||
Node.prototype.getInputValue = function (name) {
|
||||
@@ -101,7 +109,8 @@ Node.prototype.registerInputIfNeeded = function () {
|
||||
};
|
||||
|
||||
/**
|
||||
* Evaluate an expression parameter and return the coerced result
|
||||
* Evaluate an expression parameter and return the coerced result.
|
||||
* Also sets up reactive subscriptions so the node updates when dependencies change.
|
||||
*
|
||||
* @param {*} paramValue - The parameter value (might be an ExpressionParameter)
|
||||
* @param {string} portName - The input port name
|
||||
@@ -110,6 +119,14 @@ Node.prototype.registerInputIfNeeded = function () {
|
||||
Node.prototype._evaluateExpressionParameter = function (paramValue, portName) {
|
||||
// Check if this is an expression parameter
|
||||
if (!isExpressionParameter(paramValue)) {
|
||||
// Clean up any existing subscription for this port since it's no longer an expression
|
||||
if (this._expressionSubscriptions[portName]) {
|
||||
const sub = this._expressionSubscriptions[portName];
|
||||
if (sub && sub.unsub) {
|
||||
sub.unsub();
|
||||
}
|
||||
delete this._expressionSubscriptions[portName];
|
||||
}
|
||||
return paramValue; // Simple value, return as-is
|
||||
}
|
||||
|
||||
@@ -119,14 +136,63 @@ Node.prototype._evaluateExpressionParameter = function (paramValue, portName) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Evaluate the expression with access to context
|
||||
const result = evaluateExpression(paramValue.expression, this.context);
|
||||
// Compile and evaluate the expression
|
||||
// Note: We pass undefined for modelScope - evaluateExpression will use the global Model
|
||||
const compiled = compileExpression(paramValue.expression);
|
||||
if (!compiled) {
|
||||
console.warn(`Expression compilation failed for ${this.name}.${portName}: ${paramValue.expression}`);
|
||||
return paramValue.fallback;
|
||||
}
|
||||
const result = evaluateExpression(compiled, undefined);
|
||||
|
||||
// Coerce to expected type
|
||||
const coercedValue = coerceToType(result, input.type, paramValue.fallback);
|
||||
|
||||
// Set up reactive subscription
|
||||
// Track both the unsubscribe function and the expression string
|
||||
// If expression changes, we need to re-subscribe with new dependencies
|
||||
const currentSub = this._expressionSubscriptions[portName];
|
||||
const expressionChanged = currentSub && currentSub.expression !== paramValue.expression;
|
||||
|
||||
// Unsubscribe if expression changed
|
||||
if (expressionChanged && currentSub.unsub) {
|
||||
currentSub.unsub();
|
||||
delete this._expressionSubscriptions[portName];
|
||||
}
|
||||
|
||||
// Subscribe if not subscribed or expression changed
|
||||
if (!this._expressionSubscriptions[portName]) {
|
||||
const dependencies = detectDependencies(paramValue.expression);
|
||||
const hasDependencies =
|
||||
dependencies.variables.length > 0 || dependencies.objects.length > 0 || dependencies.arrays.length > 0;
|
||||
|
||||
if (hasDependencies) {
|
||||
// Subscribe to changes - when a dependency changes, re-queue the input
|
||||
// Note: We store the expression string to detect changes later
|
||||
const unsub = subscribeToChanges(
|
||||
dependencies,
|
||||
function () {
|
||||
// Don't re-evaluate if node is deleted
|
||||
if (this._deleted) return;
|
||||
|
||||
// Re-queue the expression parameter - it will be re-evaluated
|
||||
// Use the stored input value which has the current expression
|
||||
const currentValue = this._inputValues[portName];
|
||||
if (isExpressionParameter(currentValue)) {
|
||||
this.queueInput(portName, currentValue);
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
this._expressionSubscriptions[portName] = {
|
||||
unsub: unsub,
|
||||
expression: paramValue.expression
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any previous expression errors
|
||||
if (this.context.editorConnection) {
|
||||
if (this.context && this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
@@ -140,7 +206,7 @@ Node.prototype._evaluateExpressionParameter = function (paramValue, portName) {
|
||||
console.warn(`Expression evaluation failed for ${this.name}.${portName}:`, error);
|
||||
|
||||
// Show warning in editor
|
||||
if (this.context.editorConnection) {
|
||||
if (this.context && this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
@@ -605,6 +671,15 @@ Node.prototype._onNodeDeleted = function () {
|
||||
|
||||
this._deleted = true;
|
||||
|
||||
// Clean up expression subscriptions
|
||||
for (const portName in this._expressionSubscriptions) {
|
||||
const sub = this._expressionSubscriptions[portName];
|
||||
if (sub && sub.unsub) {
|
||||
sub.unsub();
|
||||
}
|
||||
}
|
||||
this._expressionSubscriptions = {};
|
||||
|
||||
for (const deleteListener of this._deleteListeners) {
|
||||
deleteListener.call(this);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user