@heswell/vuu-data-engine-local 3.0.0 → 4.0.0
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.
- package/91.js +248 -0
- package/README.md +49 -18
- package/index.js +114 -395
- package/legacy.js +5 -2
- package/package.json +6 -2
- package/react.js +64 -0
- package/src/core/engine/LocalEngine.d.ts +2 -1
- package/src/core/module/VuuModule.d.ts +12 -0
- package/src/core/table/TableContainer.d.ts +7 -4
- package/src/index.d.ts +0 -2
- package/src/legacy.d.ts +1 -0
- package/src/react.d.ts +7 -0
package/91.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { JoinTable, Table } from "@heswell/vuu-table";
|
|
2
|
+
import { EventEmitter } from "@vuu-ui/vuu-utils";
|
|
3
|
+
class Table_Table extends EventEmitter {
|
|
4
|
+
engineTable;
|
|
5
|
+
#dataMap;
|
|
6
|
+
#indexOfKey;
|
|
7
|
+
#updateColumnName;
|
|
8
|
+
constructor(schemaOrEngineTable, data = [], dataMap, updateGenerator){
|
|
9
|
+
super();
|
|
10
|
+
this.engineTable = schemaOrEngineTable instanceof Table ? schemaOrEngineTable : new Table(schemaOrEngineTable);
|
|
11
|
+
this.#dataMap = dataMap ?? this.engineTable.columnMap;
|
|
12
|
+
this.#indexOfKey = this.engineTable.indexOfKeyField;
|
|
13
|
+
for (const row of data)this.engineTable.insert(row);
|
|
14
|
+
this.engineTable.addListener({
|
|
15
|
+
onInsert: (_rowIdx, row)=>this.emit("insert", row),
|
|
16
|
+
onUpdate: (_rowIdx, row)=>this.emit("update", row, this.#updateColumnName),
|
|
17
|
+
onDelete: (_rowIdx, row)=>this.emit("delete", String(row[this.#indexOfKey])),
|
|
18
|
+
onClear: ()=>void 0
|
|
19
|
+
});
|
|
20
|
+
updateGenerator?.setTable(this);
|
|
21
|
+
updateGenerator?.setRange({
|
|
22
|
+
from: 0,
|
|
23
|
+
to: 100
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
get data() {
|
|
27
|
+
return this.engineTable.rows;
|
|
28
|
+
}
|
|
29
|
+
get map() {
|
|
30
|
+
return this.#dataMap;
|
|
31
|
+
}
|
|
32
|
+
get schema() {
|
|
33
|
+
return this.engineTable.schema;
|
|
34
|
+
}
|
|
35
|
+
get name() {
|
|
36
|
+
return this.engineTable.name;
|
|
37
|
+
}
|
|
38
|
+
get rowCount() {
|
|
39
|
+
return this.engineTable.rowCount;
|
|
40
|
+
}
|
|
41
|
+
delete(key) {
|
|
42
|
+
if (!this.engineTable.delete(key)) throw Error(`[Table] delete key ${key} not found`);
|
|
43
|
+
}
|
|
44
|
+
insert(row, _emitEvent = true) {
|
|
45
|
+
const ts = Date.now();
|
|
46
|
+
const { vuuCreatedTimestamp, vuuUpdatedTimestamp } = this.#dataMap;
|
|
47
|
+
if (void 0 !== vuuUpdatedTimestamp) row[vuuUpdatedTimestamp] = ts;
|
|
48
|
+
if (void 0 !== vuuCreatedTimestamp) row[vuuCreatedTimestamp] = ts;
|
|
49
|
+
this.engineTable.insert(row);
|
|
50
|
+
}
|
|
51
|
+
findByKey(key) {
|
|
52
|
+
return this.engineTable.getRowAtKey(key, false);
|
|
53
|
+
}
|
|
54
|
+
update(key, columnName, value) {
|
|
55
|
+
const rowIdx = this.engineTable.rowIndexAtKey(key);
|
|
56
|
+
if (-1 !== rowIdx) {
|
|
57
|
+
const newRow = this.engineTable.rows[rowIdx].slice();
|
|
58
|
+
newRow[this.#dataMap[columnName]] = value;
|
|
59
|
+
this.#updateColumnName = columnName;
|
|
60
|
+
try {
|
|
61
|
+
this.engineTable.update(rowIdx, newRow);
|
|
62
|
+
} finally{
|
|
63
|
+
this.#updateColumnName = void 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
updateRow(row) {
|
|
68
|
+
const rowIdx = this.engineTable.rowIndexAtKey(String(row[this.#indexOfKey]));
|
|
69
|
+
if (-1 !== rowIdx) this.engineTable.update(rowIdx, row);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function buildDataColumnMapFromSchema(schema) {
|
|
73
|
+
const map = {};
|
|
74
|
+
schema.columns.forEach((col, index)=>{
|
|
75
|
+
map[col.name] = index;
|
|
76
|
+
});
|
|
77
|
+
return map;
|
|
78
|
+
}
|
|
79
|
+
function buildDataColumnMap(schemas, tableName) {
|
|
80
|
+
return buildDataColumnMapFromSchema(schemas[tableName]);
|
|
81
|
+
}
|
|
82
|
+
const DEFAULT_RANGE_LIMITS = {
|
|
83
|
+
maxRangeEnd: 1000000,
|
|
84
|
+
maxRangeWidth: 1000
|
|
85
|
+
};
|
|
86
|
+
const tableKey = ({ module, table })=>`${module}:${table}`;
|
|
87
|
+
const createTable = (schema, data = [], dataMap = buildDataColumnMapFromSchema(schema), updateGenerator)=>new Table_Table(schema, data, dataMap, updateGenerator);
|
|
88
|
+
const createJoinTable = (joinTable, table1, table2, joinColumn, joinType = "inner")=>{
|
|
89
|
+
const { schema: schema1 } = table1;
|
|
90
|
+
const { schema: schema2 } = table2;
|
|
91
|
+
const combinedColumns = new Set([
|
|
92
|
+
...schema1.columns,
|
|
93
|
+
...schema2.columns
|
|
94
|
+
].map((col)=>col.name).sort());
|
|
95
|
+
const combinedSchema = {
|
|
96
|
+
key: schema1.key,
|
|
97
|
+
table: joinTable,
|
|
98
|
+
rangeLimits: DEFAULT_RANGE_LIMITS,
|
|
99
|
+
columns: Array.from(combinedColumns).map((columnName)=>({
|
|
100
|
+
name: columnName,
|
|
101
|
+
serverDataType: getServerDataType(columnName, schema1, schema2)
|
|
102
|
+
}))
|
|
103
|
+
};
|
|
104
|
+
const engineJoinTable = new JoinTable({
|
|
105
|
+
schema: combinedSchema,
|
|
106
|
+
baseTable: table1.engineTable,
|
|
107
|
+
joinTable: table2.engineTable,
|
|
108
|
+
leftColumn: joinColumn,
|
|
109
|
+
rightColumn: joinColumn,
|
|
110
|
+
joinType
|
|
111
|
+
});
|
|
112
|
+
return new Table_Table(engineJoinTable);
|
|
113
|
+
};
|
|
114
|
+
class TableContainer {
|
|
115
|
+
#tables = new Map();
|
|
116
|
+
createTable = (schema, data = [], dataMap = buildDataColumnMapFromSchema(schema), updateGenerator)=>{
|
|
117
|
+
const table = createTable(schema, data, dataMap, updateGenerator);
|
|
118
|
+
this.addTable(table);
|
|
119
|
+
return table;
|
|
120
|
+
};
|
|
121
|
+
createJoinTable(joinTable, table1, table2, joinColumn, joinType = "inner") {
|
|
122
|
+
const newTable = createJoinTable(joinTable, this.getTable(table1), this.getTable(table2), joinColumn, joinType);
|
|
123
|
+
this.addTable(newTable);
|
|
124
|
+
return newTable;
|
|
125
|
+
}
|
|
126
|
+
addTable(table, as = table.schema.table) {
|
|
127
|
+
this.#tables.set(tableKey(as), table);
|
|
128
|
+
}
|
|
129
|
+
removeTable(table) {
|
|
130
|
+
return this.#tables.delete(tableKey(table));
|
|
131
|
+
}
|
|
132
|
+
hasTable(table) {
|
|
133
|
+
return this.#tables.has(tableKey(table));
|
|
134
|
+
}
|
|
135
|
+
getTable(table) {
|
|
136
|
+
if ("string" == typeof table) {
|
|
137
|
+
const matches = Array.from(this.#tables.keys()).filter((key)=>key.endsWith(`:${table}`));
|
|
138
|
+
if (1 === matches.length) return this.#tables.get(matches[0]);
|
|
139
|
+
if (matches.length > 1) throw Error(`[TableContainer] table name ${table} is ambiguous (${matches.join(", ")}), use {module, table}`);
|
|
140
|
+
throw Error(`[TableContainer] no table ${table}`);
|
|
141
|
+
}
|
|
142
|
+
const found = this.#tables.get(tableKey(table));
|
|
143
|
+
if (found) return found;
|
|
144
|
+
throw Error(`[TableContainer] no table ${tableKey(table)}`);
|
|
145
|
+
}
|
|
146
|
+
get tables() {
|
|
147
|
+
return Array.from(this.#tables.values());
|
|
148
|
+
}
|
|
149
|
+
clear() {
|
|
150
|
+
this.#tables.clear();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const getServerDataType = (columnName, { columns: cols1, table: t1 }, { columns: cols2, table: t2 })=>{
|
|
154
|
+
const col1 = cols1.find((col)=>col.name === columnName);
|
|
155
|
+
const col2 = cols2.find((col)=>col.name === columnName);
|
|
156
|
+
if (col1 && col2) if (col1.serverDataType === col2.serverDataType) return col1.serverDataType;
|
|
157
|
+
else throw Error(`both tables ${t1.table} and ${t2.table} implement column ${columnName}, but with types differ`);
|
|
158
|
+
if (col1) return col1.serverDataType;
|
|
159
|
+
if (col2) return col2.serverDataType;
|
|
160
|
+
throw Error("how is this possible");
|
|
161
|
+
};
|
|
162
|
+
class LocalEngine {
|
|
163
|
+
tableContainer = new TableContainer();
|
|
164
|
+
#modules = new Map();
|
|
165
|
+
#disposed = false;
|
|
166
|
+
constructor({ modules = [] } = {}){
|
|
167
|
+
for (const module of modules)this.register(module);
|
|
168
|
+
}
|
|
169
|
+
register(module) {
|
|
170
|
+
const existing = this.#modules.get(module.name);
|
|
171
|
+
if (existing === module) return module;
|
|
172
|
+
if (existing) this.unregister(existing.name);
|
|
173
|
+
this.#modules.set(module.name, module);
|
|
174
|
+
for (const [tableName, table] of Object.entries(module.getTables()))this.tableContainer.addTable(table, {
|
|
175
|
+
module: module.name,
|
|
176
|
+
table: tableName
|
|
177
|
+
});
|
|
178
|
+
module.attachEngine(this);
|
|
179
|
+
return module;
|
|
180
|
+
}
|
|
181
|
+
unregister(moduleName) {
|
|
182
|
+
const module = this.#modules.get(moduleName);
|
|
183
|
+
if (module) {
|
|
184
|
+
this.#modules.delete(moduleName);
|
|
185
|
+
for (const tableName of module.getTableList())this.tableContainer.removeTable({
|
|
186
|
+
module: moduleName,
|
|
187
|
+
table: tableName
|
|
188
|
+
});
|
|
189
|
+
module.detachEngine(this);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
hasModule(moduleName) {
|
|
195
|
+
return this.#modules.has(moduleName);
|
|
196
|
+
}
|
|
197
|
+
getModule(moduleName) {
|
|
198
|
+
const module = this.#modules.get(moduleName);
|
|
199
|
+
if (module) return module;
|
|
200
|
+
throw Error(`[LocalEngine] module ${moduleName} not found`);
|
|
201
|
+
}
|
|
202
|
+
get modules() {
|
|
203
|
+
return Array.from(this.#modules.values());
|
|
204
|
+
}
|
|
205
|
+
get moduleNames() {
|
|
206
|
+
return Array.from(this.#modules.keys());
|
|
207
|
+
}
|
|
208
|
+
getTableList() {
|
|
209
|
+
return this.modules.flatMap((module)=>module.getTableList().map((table)=>({
|
|
210
|
+
module: module.name,
|
|
211
|
+
table
|
|
212
|
+
})));
|
|
213
|
+
}
|
|
214
|
+
getTableSchema({ module, table }) {
|
|
215
|
+
return this.getModule(module).getTableSchema(table);
|
|
216
|
+
}
|
|
217
|
+
createDataSource({ module, table }, viewport, config) {
|
|
218
|
+
return this.getModule(module).createDataSource(table, viewport, config);
|
|
219
|
+
}
|
|
220
|
+
get disposed() {
|
|
221
|
+
return this.#disposed;
|
|
222
|
+
}
|
|
223
|
+
dispose() {
|
|
224
|
+
for (const module of this.modules){
|
|
225
|
+
const owned = module.engine === this;
|
|
226
|
+
this.unregister(module.name);
|
|
227
|
+
if (owned) module.dispose();
|
|
228
|
+
}
|
|
229
|
+
this.tableContainer.clear();
|
|
230
|
+
this.#disposed = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const createLocalEngine = (options)=>new LocalEngine(options);
|
|
234
|
+
let defaultEngine;
|
|
235
|
+
const getDefaultEngine = ()=>{
|
|
236
|
+
if (void 0 === defaultEngine || defaultEngine.disposed) defaultEngine = new LocalEngine();
|
|
237
|
+
return defaultEngine;
|
|
238
|
+
};
|
|
239
|
+
const resetDefaultEngine = ()=>{
|
|
240
|
+
defaultEngine?.dispose();
|
|
241
|
+
defaultEngine = void 0;
|
|
242
|
+
};
|
|
243
|
+
const ensureVuuModule = (module)=>{
|
|
244
|
+
const engine = getDefaultEngine();
|
|
245
|
+
if (!engine.hasModule(module.name)) engine.register(module);
|
|
246
|
+
return module;
|
|
247
|
+
};
|
|
248
|
+
export { LocalEngine, TableContainer, Table_Table as Table, buildDataColumnMap, buildDataColumnMapFromSchema, createJoinTable, createLocalEngine, createTable, ensureVuuModule, getDefaultEngine, resetDefaultEngine };
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ two packages don't depend on each other.
|
|
|
14
14
|
All `@vuu-ui` packages are peers declared as `^4.0.0-alpha.20`, so the engine uses the host
|
|
15
15
|
application's single copy of each (a second `@vuu-ui/vuu-utils` or `@vuu-ui/core` would mean
|
|
16
16
|
a second set of React contexts). They are `@vuu-ui/vuu-utils` and `@vuu-ui/vuu-filter-parser`
|
|
17
|
-
(runtime), `@vuu-ui/core` (React provider), and the type-only `vuu-data-types`,
|
|
17
|
+
(runtime), `@vuu-ui/core` (React provider, `/react` entry only), and the type-only `vuu-data-types`,
|
|
18
18
|
`vuu-filter-types`, `vuu-protocol-types` and `vuu-table-types`.
|
|
19
19
|
|
|
20
20
|
## Architecture
|
|
@@ -33,14 +33,17 @@ a second set of React contexts). They are `@vuu-ui/vuu-utils` and `@vuu-ui/vuu-f
|
|
|
33
33
|
## Usage
|
|
34
34
|
|
|
35
35
|
Importing the package has no side effects: no modules, tables or timers are created
|
|
36
|
-
until you ask for them.
|
|
36
|
+
until you ask for them. The main entry needs no DOM, so it runs in plain Node or Bun. The
|
|
37
|
+
React bindings (`createDataSourceProvider`, `createLocalVuuServer`,
|
|
38
|
+
`LocalDataSourceProvider`) are in `@heswell/vuu-data-engine-local/react`, because
|
|
39
|
+
`@vuu-ui/core` needs a DOM (`ResizeObserver`) at import time.
|
|
37
40
|
|
|
38
41
|
```ts
|
|
39
42
|
import {
|
|
40
43
|
createLocalEngine,
|
|
41
|
-
createLocalVuuServer,
|
|
42
44
|
registerDemoModules,
|
|
43
45
|
} from "@heswell/vuu-data-engine-local";
|
|
46
|
+
import { createLocalVuuServer } from "@heswell/vuu-data-engine-local/react";
|
|
44
47
|
|
|
45
48
|
// Your own modules, in an isolated engine
|
|
46
49
|
const engine = createLocalEngine({ modules: [new OrdersModule()] });
|
|
@@ -64,20 +67,20 @@ const { simulModule } = registerDemoModules(createLocalEngine());
|
|
|
64
67
|
|
|
65
68
|
### Engines
|
|
66
69
|
|
|
67
|
-
| API
|
|
68
|
-
|
|
|
69
|
-
| `createLocalEngine({ modules? })`
|
|
70
|
-
| `engine.register(module)` / `unregister(name)`
|
|
71
|
-
| `engine.getModule(name)`, `hasModule`, `modules`
|
|
72
|
-
| `engine.getTableList()`
|
|
73
|
-
| `engine.getTableSchema(vuuTable)`
|
|
74
|
-
| `engine.createDataSource(vuuTable, vp?, cfg?)`
|
|
75
|
-
| `engine.tableContainer`
|
|
76
|
-
| `engine.dispose()`
|
|
77
|
-
| `createDataSourceProvider(engine)` | A React provider installing the legacy and core data contexts for the engine. |
|
|
78
|
-
| `createLocalVuuServer({ connectionId, modules, engine? })` | A `LocalVuuServer` (`{connectionId, DataSourceProvider, engine}`). Uses a new engine unless one is passed. |
|
|
79
|
-
| `registerDemoModules(engine?)`
|
|
80
|
-
| `getDefaultEngine()` / `resetDefaultEngine()`
|
|
70
|
+
| API | Description |
|
|
71
|
+
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
72
|
+
| `createLocalEngine({ modules? })` | A new, isolated `LocalEngine`. Two engines, or two modules in one engine, can use the same table names. |
|
|
73
|
+
| `engine.register(module)` / `unregister(name)` | Add or remove a module and its tables. A module is attached to the first engine it is registered with. |
|
|
74
|
+
| `engine.getModule(name)`, `hasModule`, `modules` | Look up modules. |
|
|
75
|
+
| `engine.getTableList()` | `VuuTable[]` (`{module, table}`) for every registered module. |
|
|
76
|
+
| `engine.getTableSchema(vuuTable)` | A table's schema. |
|
|
77
|
+
| `engine.createDataSource(vuuTable, vp?, cfg?)` | A `ModuleDataSource` for a table. |
|
|
78
|
+
| `engine.tableContainer` | The engine's `TableContainer`. Tables are keyed by `{module, table}`; a bare table name is accepted only if exactly one module has a table with that name. |
|
|
79
|
+
| `engine.dispose()` | Unregisters every module and disposes those attached to this engine (stopping ticking data and timers). |
|
|
80
|
+
| `createDataSourceProvider(engine)` (`/react`) | A React provider installing the legacy and core data contexts for the engine. |
|
|
81
|
+
| `createLocalVuuServer({ connectionId, modules, engine? })` (`/react`) | A `LocalVuuServer` (`{connectionId, DataSourceProvider, engine}`). Uses a new engine unless one is passed. |
|
|
82
|
+
| `registerDemoModules(engine?)` | Creates the SIMUL, BASKET and TEST demo modules and registers them with `engine` (default: the default engine). |
|
|
83
|
+
| `getDefaultEngine()` / `resetDefaultEngine()` | The shared engine behind the deprecated globals below. |
|
|
81
84
|
|
|
82
85
|
### Edit sessions: hooks and service overrides
|
|
83
86
|
|
|
@@ -115,6 +118,14 @@ class OrdersModule extends VuuModule<"parentOrders" | "childOrders"> {
|
|
|
115
118
|
}
|
|
116
119
|
```
|
|
117
120
|
|
|
121
|
+
`deleteRow` and `deleteSelectedRows` behave as on vuu-server: they are only valid in an edit
|
|
122
|
+
session, and mark the session row `vuuAction = "deleteRow"` whatever the `mode` (`"hard"`,
|
|
123
|
+
vuu-ui's default, or `"soft"`). The row stays in the session until it is saved, when
|
|
124
|
+
`applySessionSave` receives it as a `deleteRow` change, or `undoRowChange` restores it. A
|
|
125
|
+
row added in the session is simply dropped. Outside a session `deleteRow` returns an error;
|
|
126
|
+
`deleteSourceRow` (protected, not registered) keeps the old direct delete for subclasses
|
|
127
|
+
such as SIMUL's `DELETE_ROW`.
|
|
128
|
+
|
|
118
129
|
`endEditSession({ save: true })`:
|
|
119
130
|
|
|
120
131
|
1. Rows already handled by `beforeSessionSave` (with a `vuuMsg`) are skipped.
|
|
@@ -131,6 +142,15 @@ class OrdersModule extends VuuModule<"parentOrders" | "childOrders"> {
|
|
|
131
142
|
4. `afterSessionSave`, then the session rows are reset (deleted rows removed, `vuuAction`
|
|
132
143
|
and `vuuMsg` cleared, snapshots refreshed).
|
|
133
144
|
|
|
145
|
+
### Join tables
|
|
146
|
+
|
|
147
|
+
`createJoinTable(joinTable, table1, table2, joinColumn, joinType = "inner")` (and
|
|
148
|
+
`tableContainer.createJoinTable`) creates a join that is kept up to date as either table
|
|
149
|
+
changes. Pass `"leftOuter"` to keep every `table1` row. For a row with no match, the
|
|
150
|
+
`table2` columns are `null` (not `0`), as on vuu-server, and vuu-ui renders them empty.
|
|
151
|
+
Sort places nulls first when ascending; relational filters (`<`, `>=`, …) never match
|
|
152
|
+
null; and aggregations skip nulls.
|
|
153
|
+
|
|
134
154
|
### Ranges
|
|
135
155
|
|
|
136
156
|
`subscribe({ range })`, `dataSource.range = …` and `setRange` accept a vuu-utils `Range` or
|
|
@@ -139,12 +159,23 @@ a plain `{from, to}`.
|
|
|
139
159
|
### Deprecated globals and the `/legacy` entry
|
|
140
160
|
|
|
141
161
|
`tableContainer`, `LocalDataSourceProvider` and `ensureVuuModule` use the default engine
|
|
142
|
-
(`getDefaultEngine()`). `@heswell/vuu-data-engine-local/legacy` re-exports the main
|
|
162
|
+
(`getDefaultEngine()`). `@heswell/vuu-data-engine-local/legacy` re-exports the main and
|
|
163
|
+
`/react` entries and,
|
|
143
164
|
on import, registers the demo modules with the default engine, exporting the instances
|
|
144
165
|
`simulModule`, `basketModule`, `testModule`, `moduleAdminModule`, `userAdminModule` and
|
|
145
166
|
`notificationsModule` as 2.x did. It is the only entry point with side effects
|
|
146
167
|
(`"sideEffects": ["./src/legacy.ts"]`).
|
|
147
168
|
|
|
169
|
+
## Migrating from 3.x
|
|
170
|
+
|
|
171
|
+
These are breaking changes; the next release should be 4.0.0.
|
|
172
|
+
|
|
173
|
+
| 3.x | 4.x |
|
|
174
|
+
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
175
|
+
| `createDataSourceProvider`, `createLocalVuuServer`, `LocalDataSourceProvider` exported by the main entry. | Import them from `@heswell/vuu-data-engine-local/react` (or `/legacy`). The main entry no longer needs a DOM. |
|
|
176
|
+
| `deleteRow` with `mode: "hard"` removed the session row, so the save never saw the delete. | The row is marked `vuuAction = "deleteRow"` in either mode and applied on save (or restored by `undoRowChange`). |
|
|
177
|
+
| `deleteRow` outside an edit session deleted the source row. | Returns an error, as vuu-server does. Use an edit session, or expose the protected `deleteSourceRow` as a service. |
|
|
178
|
+
|
|
148
179
|
## Migrating from 2.x
|
|
149
180
|
|
|
150
181
|
These are breaking changes; the next release should be 3.0.0.
|