@ganju/utils 0.0.21 → 0.0.22
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/dist/data.d.ts +4 -4
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +16 -2
- package/dist/dataApi.d.ts +32 -2
- package/dist/dataApi.d.ts.map +1 -1
- package/dist/dataApi.js +11 -1
- package/dist/dataCompiler.d.ts +1 -0
- package/dist/dataCompiler.d.ts.map +1 -1
- package/dist/dataCompiler.js +3 -2
- package/dist/dataEngine.d.ts +17 -1
- package/dist/dataEngine.d.ts.map +1 -1
- package/dist/dataEngine.js +368 -10
- package/dist/dataImport.d.ts +3 -0
- package/dist/dataImport.d.ts.map +1 -1
- package/dist/dataImport.js +12 -2
- package/dist/dataRpc.d.ts +9 -1
- package/dist/dataRpc.d.ts.map +1 -1
- package/dist/dataSheet.d.ts +34 -0
- package/dist/dataSheet.d.ts.map +1 -0
- package/dist/dataSheet.js +100 -0
- package/dist/dataTriggers.d.ts +43 -4
- package/dist/dataTriggers.d.ts.map +1 -1
- package/dist/dataTriggers.js +401 -19
- package/dist/dataTypes.d.ts +23 -0
- package/dist/dataTypes.d.ts.map +1 -1
- package/dist/dataTypes.js +15 -1
- package/dist/index.d.ts +35 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/localizeDataError.d.ts.map +1 -1
- package/dist/localizeDataError.js +45 -1
- package/dist/schema.d.ts +33 -4
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +26 -3
- package/dist/untrustedContent.d.ts +7 -0
- package/dist/untrustedContent.d.ts.map +1 -1
- package/dist/untrustedContent.js +13 -1
- package/package.json +1 -1
- package/src/data.ts +19 -3
- package/src/dataApi.ts +14 -2
- package/src/dataCompiler.ts +4 -1
- package/src/dataEngine.ts +485 -10
- package/src/dataImport.ts +16 -1
- package/src/dataRpc.ts +10 -1
- package/src/dataSheet.ts +147 -0
- package/src/dataTriggers.ts +528 -19
- package/src/dataTypes.ts +46 -0
- package/src/index.ts +4 -0
- package/src/localizeDataError.ts +82 -1
- package/src/schema.ts +27 -3
- package/src/untrustedContent.ts +15 -0
package/dist/dataEngine.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.exportRows = exports.dropColumn = exports.updateColumn = exports.copyTableStep = exports.clearFailedTableCopy = exports.writeReachesCopy = exports.readTableCopy = exports.dropColumnNeedsRebuild = exports.columnChangeNeedsRebuild = exports.addColumn = exports.dropTable = exports.updateTable = exports.createTable = exports.pruneHistory = exports.rowHistory = exports.replaceFileLinks = exports.fileLinks = exports.deleteImpact = exports.write = exports.assertWritable = exports.assertActor = exports.query = exports.describe = exports.translateSqliteError = exports.normaliseColumn = exports.toTableInput = exports.loadSchema = exports.migrateInternal = exports.setMeta = exports.getMeta = exports.hasInternalTables = exports.INTERNAL_SCHEMA_VERSION = exports.INTERNAL_MIGRATIONS = void 0;
|
|
3
|
+
exports.exportRows = exports.dropColumn = exports.updateColumn = exports.copyTableStep = exports.clearFailedTableCopy = exports.writeReachesCopy = exports.readTableCopy = exports.dropColumnNeedsRebuild = exports.columnChangeNeedsRebuild = exports.addColumn = exports.dropTable = exports.updateTable = exports.createTable = exports.pruneHistory = exports.rowHistory = exports.replaceFileLinks = exports.fileLinks = exports.deleteImpact = exports.recordSyncFailure = exports.syncRows = exports.write = exports.assertNotSynced = exports.assertWritable = exports.assertActor = exports.query = exports.describe = exports.translateSqliteError = exports.normaliseColumn = exports.toTableInput = exports.loadSchema = exports.migrateInternal = exports.setMeta = exports.getMeta = exports.hasInternalTables = exports.INTERNAL_SCHEMA_VERSION = exports.INTERNAL_MIGRATIONS = void 0;
|
|
4
4
|
// What a project's database does, written against a tiny SQL runner so the
|
|
5
5
|
// same code runs inside the Durable Object (apps/data) and on node:sqlite in
|
|
6
6
|
// the tests and the tool-design eval. The runner executes; everything about
|
|
@@ -130,6 +130,26 @@ exports.INTERNAL_MIGRATIONS = [
|
|
|
130
130
|
message TEXT NOT NULL,
|
|
131
131
|
created_at TEXT NOT NULL DEFAULT (${dataDdl_1.DATA_NOW_SQL})
|
|
132
132
|
)`
|
|
133
|
+
],
|
|
134
|
+
// Time triggers: when each fires (`schedule`), how far its scans have got
|
|
135
|
+
// (`scan_state`), and the rows already fired for a due time, so none fires
|
|
136
|
+
// twice for the same one.
|
|
137
|
+
[
|
|
138
|
+
'ALTER TABLE _ganju_triggers ADD COLUMN schedule TEXT',
|
|
139
|
+
'ALTER TABLE _ganju_triggers ADD COLUMN scan_state TEXT',
|
|
140
|
+
`CREATE TABLE IF NOT EXISTS _ganju_trigger_fired (
|
|
141
|
+
trigger_id TEXT NOT NULL,
|
|
142
|
+
row_id INTEGER NOT NULL,
|
|
143
|
+
due_at TEXT NOT NULL,
|
|
144
|
+
PRIMARY KEY (trigger_id, row_id, due_at)
|
|
145
|
+
) WITHOUT ROWID`,
|
|
146
|
+
'CREATE INDEX IF NOT EXISTS _ganju_trigger_fired_due ON _ganju_trigger_fired(due_at)'
|
|
147
|
+
],
|
|
148
|
+
// Tables synced from a spreadsheet: where from (`source`), and how the
|
|
149
|
+
// last sync went (`sync_state`).
|
|
150
|
+
[
|
|
151
|
+
'ALTER TABLE _ganju_tables ADD COLUMN source TEXT',
|
|
152
|
+
'ALTER TABLE _ganju_tables ADD COLUMN sync_state TEXT'
|
|
133
153
|
]
|
|
134
154
|
];
|
|
135
155
|
exports.INTERNAL_SCHEMA_VERSION = exports.INTERNAL_MIGRATIONS.length;
|
|
@@ -150,6 +170,17 @@ const setMeta = (runner, key, value) => {
|
|
|
150
170
|
runner.all('INSERT INTO _ganju_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value', [key, value]);
|
|
151
171
|
};
|
|
152
172
|
exports.setMeta = setMeta;
|
|
173
|
+
// SQLite has no ADD COLUMN IF NOT EXISTS: a column already there is
|
|
174
|
+
// skipped, so every version can run again like the CREATE ... IF NOT EXISTS
|
|
175
|
+
// ones.
|
|
176
|
+
const ADD_COLUMN = /^ALTER TABLE (\w+) ADD COLUMN (\w+) /;
|
|
177
|
+
const runMigration = (runner, statement) => {
|
|
178
|
+
const add = ADD_COLUMN.exec(statement);
|
|
179
|
+
if (add && runner.all(`PRAGMA table_info(${(0, dataIdentifiers_1.quoteIdentifier)(add[1])})`).some(c => c.name === add[2])) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
runner.all(statement);
|
|
183
|
+
};
|
|
153
184
|
// Brings our own tables up to date. Returns the version now in force.
|
|
154
185
|
const migrateInternal = (runner) => runner.transaction(() => {
|
|
155
186
|
const current = (0, exports.hasInternalTables)(runner)
|
|
@@ -157,7 +188,7 @@ const migrateInternal = (runner) => runner.transaction(() => {
|
|
|
157
188
|
: 0;
|
|
158
189
|
for (let v = current; v < exports.INTERNAL_MIGRATIONS.length; v++) {
|
|
159
190
|
for (const statement of exports.INTERNAL_MIGRATIONS[v])
|
|
160
|
-
runner
|
|
191
|
+
runMigration(runner, statement);
|
|
161
192
|
}
|
|
162
193
|
if (current < exports.INTERNAL_MIGRATIONS.length) {
|
|
163
194
|
(0, exports.setMeta)(runner, 'schema_version', String(exports.INTERNAL_MIGRATIONS.length));
|
|
@@ -190,6 +221,8 @@ const loadSchema = (runner) => {
|
|
|
190
221
|
customerScoped: row.customer_scoped === 1,
|
|
191
222
|
history: row.history === 1,
|
|
192
223
|
position: Number(row.position),
|
|
224
|
+
source: parseJson(row.source ?? null),
|
|
225
|
+
sync: parseJson(row.sync_state ?? null),
|
|
193
226
|
columns: columns
|
|
194
227
|
.filter(c => c.table_id === row.id)
|
|
195
228
|
.map(c => {
|
|
@@ -389,6 +422,47 @@ const normaliseAccess = (value, fallback) => {
|
|
|
389
422
|
}
|
|
390
423
|
return value;
|
|
391
424
|
};
|
|
425
|
+
// A table's sheet source, checked against the columns it will have. Null
|
|
426
|
+
// stops syncing.
|
|
427
|
+
const normaliseSource = (value, tableName, columns, customerScoped) => {
|
|
428
|
+
if (value === undefined || value === null)
|
|
429
|
+
return null;
|
|
430
|
+
// A sheet's rows have no customer, so a bot would never see them.
|
|
431
|
+
if (customerScoped)
|
|
432
|
+
throw syncedScopedError(tableName);
|
|
433
|
+
if (!isPlainObject(value)) {
|
|
434
|
+
throw new dataTypes_1.DataError('"source" is { "uri", "sheet"?, "key" }, or null to stop syncing.');
|
|
435
|
+
}
|
|
436
|
+
for (const key of Object.keys(value)) {
|
|
437
|
+
if (!['uri', 'sheet', 'key', 'title'].includes(key)) {
|
|
438
|
+
throw new dataTypes_1.DataError(`"source" takes uri, sheet and key, not "${key}".`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const { uri, sheet, key, title } = value;
|
|
442
|
+
if (typeof uri !== 'string' || !/^[a-z][\w+.-]*:\/\/\S+$/i.test(uri) || uri.length > dataTypes_1.DATA_FILE_URI_MAX) {
|
|
443
|
+
throw new dataTypes_1.DataError('"source.uri" is the uri of a spreadsheet or CSV in the project\'s knowledge.');
|
|
444
|
+
}
|
|
445
|
+
if (sheet !== undefined && sheet !== null && (typeof sheet !== 'string' || !sheet.trim() || sheet.length > 100)) {
|
|
446
|
+
throw new dataTypes_1.DataError('"source.sheet" is the name of a sheet in the file, up to 100 characters.');
|
|
447
|
+
}
|
|
448
|
+
const column = columns.find(c => c.name === key);
|
|
449
|
+
if (typeof key !== 'string' || !column) {
|
|
450
|
+
throw new dataTypes_1.DataError(`"source.key" is the column that names a row in both the sheet and ${tableName}. Columns: ${columns.map(c => c.name).join(', ')}.`);
|
|
451
|
+
}
|
|
452
|
+
if (!column.unique) {
|
|
453
|
+
throw new dataTypes_1.DataError(`${tableName}.${key} must be unique to be the sync key.`);
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
uri,
|
|
457
|
+
...(typeof sheet === 'string' ? { sheet: sheet.trim() } : {}),
|
|
458
|
+
key,
|
|
459
|
+
...(typeof title === 'string' && title.trim() ? { title: title.trim().slice(0, 200) } : {})
|
|
460
|
+
};
|
|
461
|
+
};
|
|
462
|
+
const syncedScopedError = (tableName) => new dataTypes_1.DataError(`${tableName} can't be both customer-scoped and synced from a sheet: synced rows have no customer, so the bots wouldn't see them.`);
|
|
463
|
+
const sameSource = (a, b) => (a?.uri ?? null) === (b?.uri ?? null) &&
|
|
464
|
+
(a?.sheet ?? null) === (b?.sheet ?? null) &&
|
|
465
|
+
(a?.key ?? null) === (b?.key ?? null);
|
|
392
466
|
// Keeping indexes, triggers and the search table in line with the metadata
|
|
393
467
|
const tableObjects = (runner, type, table) => runner
|
|
394
468
|
.all('SELECT name, sql FROM sqlite_master WHERE type = ? AND tbl_name = ?', [type, table])
|
|
@@ -1034,6 +1108,15 @@ const assertWritable = (runner, caller, addsData) => {
|
|
|
1034
1108
|
}
|
|
1035
1109
|
};
|
|
1036
1110
|
exports.assertWritable = assertWritable;
|
|
1111
|
+
// A table synced from a sheet takes its rows from the sheet alone, whoever
|
|
1112
|
+
// writes: the dashboard, the API, the CLI, functions and the assistant.
|
|
1113
|
+
const assertNotSynced = (table) => {
|
|
1114
|
+
if (!table.source)
|
|
1115
|
+
return;
|
|
1116
|
+
const from = table.source.title || table.source.uri;
|
|
1117
|
+
throw new dataTypes_1.DataError(`${table.name} is synced from "${from}"; change the sheet, or stop syncing it.`, 'read_only', { source: table.source });
|
|
1118
|
+
};
|
|
1119
|
+
exports.assertNotSynced = assertNotSynced;
|
|
1037
1120
|
// Every write names its actor here first, so row history (kept by triggers)
|
|
1038
1121
|
// records who made each change, cascades included.
|
|
1039
1122
|
const setContextActor = (runner, actor) => {
|
|
@@ -1193,6 +1276,7 @@ const runOperation = (runner, schema, caller, rawOp, index, results) => {
|
|
|
1193
1276
|
op = { ...rawOp, id };
|
|
1194
1277
|
}
|
|
1195
1278
|
const table = (0, dataCompiler_1.tableFor)(schema, caller, op.table, kind === 'insert' ? 'add' : 'full');
|
|
1279
|
+
(0, exports.assertNotSynced)(table);
|
|
1196
1280
|
const actor = caller.actor;
|
|
1197
1281
|
const scoped = (0, dataCompiler_1.isScoped)(caller, table);
|
|
1198
1282
|
if (kind === 'insert') {
|
|
@@ -1220,8 +1304,9 @@ const runOperation = (runner, schema, caller, rawOp, index, results) => {
|
|
|
1220
1304
|
// One column list for the whole batch, so rows that leave a column out
|
|
1221
1305
|
// get NULL there rather than its default only when another row sets it.
|
|
1222
1306
|
const names = [...new Set(prepared.flatMap(values => [...values.keys()]))];
|
|
1223
|
-
const columns = [...names, 'created_by', 'updated_by', ...(scoped ? ['participant_id'] : [])];
|
|
1307
|
+
const columns = ['id', ...names, 'created_by', 'updated_by', ...(scoped ? ['participant_id'] : [])];
|
|
1224
1308
|
const ids = [];
|
|
1309
|
+
let nextId = nextRowId(runner, table);
|
|
1225
1310
|
try {
|
|
1226
1311
|
for (const chunk of (0, dataCompiler_1.chunkRows)(prepared, columns.length)) {
|
|
1227
1312
|
const usesDefaults = chunk.some(values => names.some(n => !values.has(n)));
|
|
@@ -1229,11 +1314,12 @@ const runOperation = (runner, schema, caller, rawOp, index, results) => {
|
|
|
1229
1314
|
// Rows with different columns go one by one, so each missing
|
|
1230
1315
|
// column takes its default.
|
|
1231
1316
|
for (const values of chunk)
|
|
1232
|
-
ids.push(insertOne(runner, table, values, actor, scoped, caller));
|
|
1317
|
+
ids.push(insertOne(runner, table, values, actor, scoped, caller, nextId++));
|
|
1233
1318
|
continue;
|
|
1234
1319
|
}
|
|
1235
1320
|
const params = [];
|
|
1236
1321
|
for (const values of chunk) {
|
|
1322
|
+
params.push(nextId++);
|
|
1237
1323
|
for (const name of names)
|
|
1238
1324
|
params.push(values.get(name) ?? null);
|
|
1239
1325
|
params.push(actor, actor);
|
|
@@ -1336,6 +1422,26 @@ const runOperation = (runner, schema, caller, rawOp, index, results) => {
|
|
|
1336
1422
|
.all(`SELECT t.id FROM ${(0, dataIdentifiers_1.quoteIdentifier)(table.name)} t WHERE ${where}`, ctx.params)
|
|
1337
1423
|
.map(r => Number(r.id))
|
|
1338
1424
|
: assertBoundedTarget();
|
|
1425
|
+
// A delete here mustn't change a synced table's rows behind its sheet,
|
|
1426
|
+
// through a cascade or a set null.
|
|
1427
|
+
for (const other of schema.tables) {
|
|
1428
|
+
if (!other.source || other.name === table.name || ids.length === 0)
|
|
1429
|
+
continue;
|
|
1430
|
+
for (const c of other.columns) {
|
|
1431
|
+
if (c.type !== 'relation' || c.table !== table.name)
|
|
1432
|
+
continue;
|
|
1433
|
+
if ((c.onDelete ?? 'restrict') === 'restrict')
|
|
1434
|
+
continue;
|
|
1435
|
+
let n = 0;
|
|
1436
|
+
for (let i = 0; i < ids.length; i += dataTypes_1.DATA_MAX_PARAMS) {
|
|
1437
|
+
const slice = ids.slice(i, i + dataTypes_1.DATA_MAX_PARAMS);
|
|
1438
|
+
n += Number(first(runner, `SELECT count(*) AS n FROM ${(0, dataIdentifiers_1.quoteIdentifier)(other.name)} WHERE ${(0, dataIdentifiers_1.quoteIdentifier)(c.name)} IN (${slice.map(() => '?').join(', ')})`, slice)?.n ?? 0);
|
|
1439
|
+
}
|
|
1440
|
+
if (n > 0) {
|
|
1441
|
+
throw new dataTypes_1.DataError(`Can't delete: it would change ${n === 1 ? 'a row' : `${n} rows`} of ${other.name} (${other.name}.${c.name}), which is synced from "${other.source.title || other.source.uri}"; change the sheet first, or stop syncing it.`, 'read_only', { table: other.name, column: c.name, count: n });
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1339
1445
|
// A readable "3 rows in order_lines still point at this order" before
|
|
1340
1446
|
// SQLite's bare foreign key error. A row of the same table counts even
|
|
1341
1447
|
// when this delete removes it too: SQLite checks a restrict as each row
|
|
@@ -1374,9 +1480,16 @@ const runOperation = (runner, schema, caller, rawOp, index, results) => {
|
|
|
1374
1480
|
}
|
|
1375
1481
|
return { op: kind, table: table.name, changed };
|
|
1376
1482
|
};
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1483
|
+
// The id the table's next row gets. Tables have no AUTOINCREMENT, so SQLite
|
|
1484
|
+
// would hand out the highest id again once that row is deleted, and the
|
|
1485
|
+
// deleted row's history would read as the new one's. Above both, instead.
|
|
1486
|
+
const nextRowId = (runner, table) => Number(first(runner, `SELECT max(
|
|
1487
|
+
coalesce((SELECT max(id) FROM ${(0, dataIdentifiers_1.quoteIdentifier)(table.name)}), 0),
|
|
1488
|
+
coalesce((SELECT max(row_id) FROM _ganju_row_log WHERE table_id = ?), 0)
|
|
1489
|
+
) + 1 AS id`, [table.id])?.id ?? 1);
|
|
1490
|
+
const insertOne = (runner, table, values, actor, scoped, caller, id) => {
|
|
1491
|
+
const names = ['id', ...values.keys(), 'created_by', 'updated_by', ...(scoped ? ['participant_id'] : [])];
|
|
1492
|
+
const params = [id, ...values.values(), actor, actor, ...(scoped ? [(0, dataCompiler_1.participantOf)(caller)] : [])];
|
|
1380
1493
|
return Number(runner.all(`INSERT INTO ${(0, dataIdentifiers_1.quoteIdentifier)(table.name)} (${names.map(dataIdentifiers_1.quoteIdentifier).join(', ')}) VALUES (${names.map(() => '?').join(', ')}) RETURNING id`, params)[0]?.id);
|
|
1381
1494
|
};
|
|
1382
1495
|
// A list of inserts, updates and deletes across tables, applied together or
|
|
@@ -1417,6 +1530,214 @@ const write = (runner, schema, caller, rawArgs) => {
|
|
|
1417
1530
|
});
|
|
1418
1531
|
};
|
|
1419
1532
|
exports.write = write;
|
|
1533
|
+
// Whether `version` is older than what the table's rows were last made from.
|
|
1534
|
+
const staleVersion = (table, version) => typeof version === 'string' && !!table.sync?.version && version < table.sync.version;
|
|
1535
|
+
// The same JSON with its keys in order, so a sheet that writes it another
|
|
1536
|
+
// way round isn't a change.
|
|
1537
|
+
const canonicalJson = (value) => Array.isArray(value)
|
|
1538
|
+
? value.map(canonicalJson)
|
|
1539
|
+
: isPlainObject(value)
|
|
1540
|
+
? Object.fromEntries(Object.keys(value).sort().map(k => [k, canonicalJson(value[k])]))
|
|
1541
|
+
: value;
|
|
1542
|
+
const sameStored = (column, a, b) => {
|
|
1543
|
+
if (a === b)
|
|
1544
|
+
return true;
|
|
1545
|
+
if (column.type !== 'json' || typeof a !== 'string' || typeof b !== 'string')
|
|
1546
|
+
return false;
|
|
1547
|
+
try {
|
|
1548
|
+
return JSON.stringify(canonicalJson(JSON.parse(a))) === JSON.stringify(canonicalJson(JSON.parse(b)));
|
|
1549
|
+
}
|
|
1550
|
+
catch {
|
|
1551
|
+
return false;
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
const nowStamp = () => new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
1555
|
+
// A refusal about one line of the sheet.
|
|
1556
|
+
const atLine = (line, error) => new dataTypes_1.DataError(`Line ${line}: ${error.message}`, error.code, error.details);
|
|
1557
|
+
const saveSyncState = (runner, table, state, title) => {
|
|
1558
|
+
const source = table.source && typeof title === 'string' && title.trim()
|
|
1559
|
+
? { ...table.source, title: title.trim().slice(0, 200) }
|
|
1560
|
+
: table.source;
|
|
1561
|
+
runner.all('UPDATE _ganju_tables SET sync_state = ?, source = ? WHERE id = ?', [
|
|
1562
|
+
JSON.stringify(state),
|
|
1563
|
+
source ? JSON.stringify(source) : null,
|
|
1564
|
+
table.id
|
|
1565
|
+
]);
|
|
1566
|
+
};
|
|
1567
|
+
// Makes a synced table's rows what the sheet says, in one transaction: new
|
|
1568
|
+
// keys are inserted, rows whose values differ updated, rows whose key is
|
|
1569
|
+
// gone deleted. Unchanged rows aren't written. Anything that doesn't fit
|
|
1570
|
+
// refuses the whole sync, and the table keeps its rows.
|
|
1571
|
+
const syncRows = (runner, schema, caller, args) => {
|
|
1572
|
+
(0, exports.assertWritable)(runner, caller, true);
|
|
1573
|
+
const table = tableNamed(schema, args?.table);
|
|
1574
|
+
const source = table.source;
|
|
1575
|
+
if (!source) {
|
|
1576
|
+
throw new dataTypes_1.DataError(`${table.name} isn't synced from a sheet.`, 'conflict');
|
|
1577
|
+
}
|
|
1578
|
+
const keyColumn = columnNamed(table, source.key);
|
|
1579
|
+
if (!Array.isArray(args.columns) || !Array.isArray(args.rows)) {
|
|
1580
|
+
throw new dataTypes_1.DataError('A sync takes "columns" and "rows".');
|
|
1581
|
+
}
|
|
1582
|
+
const columns = [...new Set(args.columns)].map(name => columnNamed(table, name));
|
|
1583
|
+
if (!columns.includes(keyColumn)) {
|
|
1584
|
+
throw new dataTypes_1.DataError(`The sheet has no column for ${table.name}'s key, ${keyColumn.name}.`);
|
|
1585
|
+
}
|
|
1586
|
+
const unfilled = table.columns.filter(c => c.required && (c.default === undefined || c.default === null) && !columns.includes(c));
|
|
1587
|
+
if (unfilled.length) {
|
|
1588
|
+
throw new dataTypes_1.DataError(`The sheet has no column for ${unfilled.map(c => `${table.name}.${c.name}`).join(', ')}, which ${unfilled.length === 1 ? 'is' : 'are'} required.`);
|
|
1589
|
+
}
|
|
1590
|
+
if (args.rows.length > dataTypes_1.DATA_SHEET_MAX_ROWS) {
|
|
1591
|
+
throw new dataTypes_1.DataError(`The sheet has ${args.rows.length.toLocaleString('en-US')} rows; a synced table takes up to ${dataTypes_1.DATA_SHEET_MAX_ROWS.toLocaleString('en-US')}.`, 'too_large');
|
|
1592
|
+
}
|
|
1593
|
+
// Every line as it will be stored, checked before anything is written.
|
|
1594
|
+
const keyOf = (value) => JSON.stringify(value);
|
|
1595
|
+
const wanted = new Map();
|
|
1596
|
+
args.rows.forEach((row, i) => {
|
|
1597
|
+
const line = args.lines?.[i] ?? i + 2;
|
|
1598
|
+
if (!isPlainObject(row))
|
|
1599
|
+
throw atLine(line, new dataTypes_1.DataError('A row is an object of column: value.'));
|
|
1600
|
+
const values = new Map();
|
|
1601
|
+
for (const column of columns) {
|
|
1602
|
+
const raw = row[column.name];
|
|
1603
|
+
let stored;
|
|
1604
|
+
try {
|
|
1605
|
+
stored = raw === undefined || raw === null ? null : (0, dataCompiler_1.toStored)(table, column, raw, caller.timeZone);
|
|
1606
|
+
}
|
|
1607
|
+
catch (error) {
|
|
1608
|
+
if (error instanceof dataTypes_1.DataError)
|
|
1609
|
+
throw atLine(line, error);
|
|
1610
|
+
throw error;
|
|
1611
|
+
}
|
|
1612
|
+
if (stored === null && column.required) {
|
|
1613
|
+
throw atLine(line, new dataTypes_1.DataError(`${table.name}.${column.name} is required.`));
|
|
1614
|
+
}
|
|
1615
|
+
if (column.min !== null && typeof stored === 'number' && stored < column.min) {
|
|
1616
|
+
throw atLine(line, new dataTypes_1.DataError(`${table.name}.${column.name} can't go below ${column.min}.`));
|
|
1617
|
+
}
|
|
1618
|
+
values.set(column.name, stored);
|
|
1619
|
+
}
|
|
1620
|
+
const key = values.get(keyColumn.name) ?? null;
|
|
1621
|
+
if (key === null) {
|
|
1622
|
+
throw atLine(line, new dataTypes_1.DataError(`The key, ${keyColumn.name}, is empty.`));
|
|
1623
|
+
}
|
|
1624
|
+
const seen = wanted.get(keyOf(key));
|
|
1625
|
+
if (seen) {
|
|
1626
|
+
throw atLine(line, new dataTypes_1.DataError(`${keyColumn.name} ${JSON.stringify(key)} is already on line ${seen.line}; each line of the sheet needs its own key.`, 'constraint', { key, lines: [seen.line, line] }));
|
|
1627
|
+
}
|
|
1628
|
+
wanted.set(keyOf(key), { line, values });
|
|
1629
|
+
});
|
|
1630
|
+
// Two syncs that overlap: the one that read the older file changes nothing.
|
|
1631
|
+
if (staleVersion(table, args.version)) {
|
|
1632
|
+
return { ...table.sync, inserted: 0, updated: 0, deleted: 0, unchanged: 0, stale: true };
|
|
1633
|
+
}
|
|
1634
|
+
return runner.transaction(() => {
|
|
1635
|
+
setContextActor(runner, caller.actor);
|
|
1636
|
+
runner.all('DELETE FROM _ganju_changes');
|
|
1637
|
+
const before = runner.usageSoFar?.().rowsWritten ?? 0;
|
|
1638
|
+
const names = columns.map(c => c.name);
|
|
1639
|
+
const existing = new Map();
|
|
1640
|
+
const stale = [];
|
|
1641
|
+
let nextId = nextRowId(runner, table);
|
|
1642
|
+
for (const row of runner.all(`SELECT id, ${names.map(n => (0, dataIdentifiers_1.quoteIdentifier)(n)).join(', ')} FROM ${(0, dataIdentifiers_1.quoteIdentifier)(table.name)}`)) {
|
|
1643
|
+
const key = row[keyColumn.name] ?? null;
|
|
1644
|
+
if (key !== null && wanted.has(keyOf(key)))
|
|
1645
|
+
existing.set(keyOf(key), row);
|
|
1646
|
+
else
|
|
1647
|
+
stale.push(Number(row.id));
|
|
1648
|
+
}
|
|
1649
|
+
const counts = { inserted: 0, updated: 0, deleted: 0, unchanged: 0 };
|
|
1650
|
+
// Rows the sheet no longer has go first, so a key that moved to a new
|
|
1651
|
+
// line isn't taken twice. A row another table still points at with
|
|
1652
|
+
// restrict stops the sync.
|
|
1653
|
+
for (const other of schema.tables) {
|
|
1654
|
+
for (const c of other.columns) {
|
|
1655
|
+
if (c.type !== 'relation' || c.table !== table.name)
|
|
1656
|
+
continue;
|
|
1657
|
+
if ((c.onDelete ?? 'restrict') !== 'restrict' || stale.length === 0)
|
|
1658
|
+
continue;
|
|
1659
|
+
let n = 0;
|
|
1660
|
+
for (let i = 0; i < stale.length; i += dataTypes_1.DATA_MAX_PARAMS) {
|
|
1661
|
+
const slice = stale.slice(i, i + dataTypes_1.DATA_MAX_PARAMS);
|
|
1662
|
+
n += Number(first(runner, `SELECT count(*) AS n FROM ${(0, dataIdentifiers_1.quoteIdentifier)(other.name)} WHERE ${(0, dataIdentifiers_1.quoteIdentifier)(c.name)} IN (${slice.map(() => '?').join(', ')})`, slice)?.n ?? 0);
|
|
1663
|
+
}
|
|
1664
|
+
if (n > 0) {
|
|
1665
|
+
throw new dataTypes_1.DataError(`Rows in ${other.name} still point at ${n === 1 ? 'a row' : `${n} rows`} the sheet no longer has (${other.name}.${c.name}); put them back in the sheet, or change those rows first.`, 'constraint', { table: other.name, column: c.name, count: n });
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
try {
|
|
1670
|
+
for (let i = 0; i < stale.length; i += dataTypes_1.DATA_MAX_PARAMS) {
|
|
1671
|
+
const slice = stale.slice(i, i + dataTypes_1.DATA_MAX_PARAMS);
|
|
1672
|
+
counts.deleted += runner.all(`DELETE FROM ${(0, dataIdentifiers_1.quoteIdentifier)(table.name)} WHERE id IN (${slice.map(() => '?').join(', ')}) RETURNING id`, slice).length;
|
|
1673
|
+
}
|
|
1674
|
+
for (const [key, { line, values }] of wanted) {
|
|
1675
|
+
const current = existing.get(key);
|
|
1676
|
+
try {
|
|
1677
|
+
if (!current) {
|
|
1678
|
+
insertOne(runner, table, values, caller.actor, false, caller, nextId++);
|
|
1679
|
+
counts.inserted++;
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
const changed = columns
|
|
1683
|
+
.filter(c => !sameStored(c, current[c.name] ?? null, values.get(c.name) ?? null))
|
|
1684
|
+
.map(c => c.name);
|
|
1685
|
+
if (changed.length === 0) {
|
|
1686
|
+
counts.unchanged++;
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
runner.all(`UPDATE ${(0, dataIdentifiers_1.quoteIdentifier)(table.name)} SET ${changed.map(n => `${(0, dataIdentifiers_1.quoteIdentifier)(n)} = ?`).join(', ')}, version = version + 1, updated_at = ${dataDdl_1.DATA_NOW_SQL}, updated_by = ? WHERE id = ?`, [...changed.map(n => values.get(n) ?? null), caller.actor, Number(current.id)]);
|
|
1690
|
+
counts.updated++;
|
|
1691
|
+
}
|
|
1692
|
+
catch (error) {
|
|
1693
|
+
throw atLine(line, (0, exports.translateSqliteError)(schema, table, error));
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
catch (error) {
|
|
1698
|
+
throw (0, exports.translateSqliteError)(schema, table, error);
|
|
1699
|
+
}
|
|
1700
|
+
runner.noteWrite?.({
|
|
1701
|
+
table: table.name,
|
|
1702
|
+
rowsWritten: (runner.usageSoFar?.().rowsWritten ?? 0) - before
|
|
1703
|
+
});
|
|
1704
|
+
(0, dataTriggers_1.collectTriggerEvents)(runner, schema, caller);
|
|
1705
|
+
const state = {
|
|
1706
|
+
at: nowStamp(),
|
|
1707
|
+
status: 'ok',
|
|
1708
|
+
...counts,
|
|
1709
|
+
error: null,
|
|
1710
|
+
version: typeof args.version === 'string' ? args.version : (table.sync?.version ?? null)
|
|
1711
|
+
};
|
|
1712
|
+
saveSyncState(runner, table, state, args.title);
|
|
1713
|
+
return state;
|
|
1714
|
+
});
|
|
1715
|
+
};
|
|
1716
|
+
exports.syncRows = syncRows;
|
|
1717
|
+
// Keeps why a sync was refused on the table, where the owner sees it. The
|
|
1718
|
+
// rows are as they were.
|
|
1719
|
+
const recordSyncFailure = (runner, schema, args) => {
|
|
1720
|
+
const table = schema.tables.find(t => t.name === args?.table);
|
|
1721
|
+
if (!table?.source)
|
|
1722
|
+
return null;
|
|
1723
|
+
// An older file's problem says nothing about the rows now.
|
|
1724
|
+
if (staleVersion(table, args.version))
|
|
1725
|
+
return table.sync ?? null;
|
|
1726
|
+
const state = {
|
|
1727
|
+
at: nowStamp(),
|
|
1728
|
+
status: 'failed',
|
|
1729
|
+
inserted: 0,
|
|
1730
|
+
updated: 0,
|
|
1731
|
+
deleted: 0,
|
|
1732
|
+
unchanged: 0,
|
|
1733
|
+
error: String(args.error ?? '').slice(0, 1000) || 'The sync failed.',
|
|
1734
|
+
// The rows are still from the last file applied.
|
|
1735
|
+
version: table.sync?.version ?? null
|
|
1736
|
+
};
|
|
1737
|
+
saveSyncState(runner, table, state, args.title);
|
|
1738
|
+
return state;
|
|
1739
|
+
};
|
|
1740
|
+
exports.recordSyncFailure = recordSyncFailure;
|
|
1420
1741
|
// Delete preview
|
|
1421
1742
|
// What deleting `ids` of a table, or all its rows, would do to the rows that
|
|
1422
1743
|
// point at them, counted the way SQLite applies it: cascades are followed to
|
|
@@ -1739,6 +2060,7 @@ const createTable = (runner, schema, caller, input) => {
|
|
|
1739
2060
|
const access = normaliseAccess(input.access, 'read');
|
|
1740
2061
|
const customerScoped = flag(input.customerScoped, 'customerScoped');
|
|
1741
2062
|
const history = input.history === undefined ? true : flag(input.history, 'history');
|
|
2063
|
+
const source = normaliseSource(input.source, name, columns, customerScoped);
|
|
1742
2064
|
return runner.transaction(() => {
|
|
1743
2065
|
const position = Number(first(runner, 'SELECT coalesce(max(position), -1) + 1 AS p FROM _ganju_tables')?.p ?? 0);
|
|
1744
2066
|
// Never an id a dropped table had: its row history stays in the log
|
|
@@ -1748,7 +2070,7 @@ const createTable = (runner, schema, caller, input) => {
|
|
|
1748
2070
|
coalesce((SELECT max(id) FROM _ganju_tables), 0),
|
|
1749
2071
|
coalesce((SELECT max(table_id) FROM _ganju_row_log), 0)
|
|
1750
2072
|
) + 1 AS id`)?.id ?? 1);
|
|
1751
|
-
const tableId = Number(runner.all('INSERT INTO _ganju_tables (id, name, label, description, access, customer_scoped, history, position) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id', [
|
|
2073
|
+
const tableId = Number(runner.all('INSERT INTO _ganju_tables (id, name, label, description, access, customer_scoped, history, position, source) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id', [
|
|
1752
2074
|
nextId,
|
|
1753
2075
|
name,
|
|
1754
2076
|
cleanText(input.label, dataTypes_1.DATA_MAX_LABEL, `${name} label`),
|
|
@@ -1756,7 +2078,8 @@ const createTable = (runner, schema, caller, input) => {
|
|
|
1756
2078
|
access,
|
|
1757
2079
|
customerScoped ? 1 : 0,
|
|
1758
2080
|
history ? 1 : 0,
|
|
1759
|
-
position
|
|
2081
|
+
position,
|
|
2082
|
+
source ? JSON.stringify(source) : null
|
|
1760
2083
|
])[0].id);
|
|
1761
2084
|
const stored = columns.map((c, i) => ({
|
|
1762
2085
|
...c,
|
|
@@ -1777,7 +2100,7 @@ const updateTable = (runner, schema, caller, name, patch) => {
|
|
|
1777
2100
|
const table = tableNamed(schema, name);
|
|
1778
2101
|
if (!isPlainObject(patch))
|
|
1779
2102
|
throw new dataTypes_1.DataError('The change is an object.');
|
|
1780
|
-
const allowed = ['name', 'label', 'description', 'displayColumn', 'access', 'customerScoped', 'history'];
|
|
2103
|
+
const allowed = ['name', 'label', 'description', 'displayColumn', 'access', 'customerScoped', 'history', 'source'];
|
|
1781
2104
|
for (const key of Object.keys(patch)) {
|
|
1782
2105
|
if (!allowed.includes(key)) {
|
|
1783
2106
|
throw new dataTypes_1.DataError(`"${key}" can't be changed here; tables take ${allowed.join(', ')}.`);
|
|
@@ -1813,6 +2136,9 @@ const updateTable = (runner, schema, caller, name, patch) => {
|
|
|
1813
2136
|
params.push(normaliseAccess(patch.access, table.access));
|
|
1814
2137
|
}
|
|
1815
2138
|
if ('customerScoped' in patch) {
|
|
2139
|
+
const keepsSource = 'source' in patch ? patch.source !== null && patch.source !== undefined : !!table.source;
|
|
2140
|
+
if (patch.customerScoped === true && keepsSource)
|
|
2141
|
+
throw syncedScopedError(table.name);
|
|
1816
2142
|
sets.push('customer_scoped = ?');
|
|
1817
2143
|
params.push(flag(patch.customerScoped, 'customerScoped') ? 1 : 0);
|
|
1818
2144
|
}
|
|
@@ -1820,6 +2146,15 @@ const updateTable = (runner, schema, caller, name, patch) => {
|
|
|
1820
2146
|
sets.push('history = ?');
|
|
1821
2147
|
params.push(flag(patch.history, 'history') ? 1 : 0);
|
|
1822
2148
|
}
|
|
2149
|
+
if ('source' in patch) {
|
|
2150
|
+
const scoped = 'customerScoped' in patch ? patch.customerScoped === true : table.customerScoped;
|
|
2151
|
+
const source = normaliseSource(patch.source, current, table.columns, scoped);
|
|
2152
|
+
sets.push('source = ?');
|
|
2153
|
+
params.push(source ? JSON.stringify(source) : null);
|
|
2154
|
+
// A new file, sheet or key starts over: the last sync was of another.
|
|
2155
|
+
if (!sameSource(table.source, source))
|
|
2156
|
+
sets.push('sync_state = NULL');
|
|
2157
|
+
}
|
|
1823
2158
|
if (sets.length) {
|
|
1824
2159
|
runner.all(`UPDATE _ganju_tables SET ${sets.join(', ')} WHERE id = ?`, [...params, table.id]);
|
|
1825
2160
|
}
|
|
@@ -2177,7 +2512,21 @@ const updateColumn = (runner, schema, caller, tableName, columnName, patch) => {
|
|
|
2177
2512
|
if (draft.name !== column.name && table.columns.some(c => c.name === draft.name)) {
|
|
2178
2513
|
throw new dataTypes_1.DataError(`${table.name} already has a column "${draft.name}".`, 'conflict');
|
|
2179
2514
|
}
|
|
2515
|
+
// A time trigger counts from a date or datetime; anything else has no time.
|
|
2516
|
+
if (draft.type !== 'date' && draft.type !== 'datetime') {
|
|
2517
|
+
const timed = (0, dataTriggers_1.timeTriggersCountingFrom)(runner, table.id, column.name);
|
|
2518
|
+
if (timed.length) {
|
|
2519
|
+
throw new dataTypes_1.DataError(`${table.name}.${column.name} stays a date or datetime while time triggers count from it (${timed.join(', ')}); change or delete them first.`, 'constraint', { triggers: timed });
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
const isSyncKey = table.source?.key === column.name;
|
|
2523
|
+
if (isSyncKey && !draft.unique) {
|
|
2524
|
+
throw new dataTypes_1.DataError(`${table.name}.${column.name} is the key the sheet sync matches rows by, so it stays unique.`, 'constraint');
|
|
2525
|
+
}
|
|
2180
2526
|
if ((0, exports.columnChangeNeedsRebuild)(schema, table.name, column.name, patch)) {
|
|
2527
|
+
if (isSyncKey && draft.name !== column.name) {
|
|
2528
|
+
throw new dataTypes_1.DataError(`${table.name}.${column.name} is the sheet sync's key: rename it on its own, then make the other changes.`, 'constraint');
|
|
2529
|
+
}
|
|
2181
2530
|
// A rename renames the column in its triggers too; a copy can't, so the
|
|
2182
2531
|
// two go one after the other.
|
|
2183
2532
|
const triggers = draft.name !== column.name ? (0, dataTriggers_1.triggersUsingColumn)(runner, table.id, column.name) : [];
|
|
@@ -2192,6 +2541,12 @@ const updateColumn = (runner, schema, caller, tableName, columnName, patch) => {
|
|
|
2192
2541
|
runner.all(`DROP TABLE IF EXISTS ${(0, dataIdentifiers_1.quoteIdentifier)((0, dataDdl_1.ftsTableName)(table))}`);
|
|
2193
2542
|
runner.all(`ALTER TABLE ${(0, dataIdentifiers_1.quoteIdentifier)(table.name)} RENAME COLUMN ${(0, dataIdentifiers_1.quoteIdentifier)(column.name)} TO ${(0, dataIdentifiers_1.quoteIdentifier)(draft.name)}`);
|
|
2194
2543
|
(0, dataTriggers_1.renameColumnInTriggers)(runner, table.id, column.name, draft.name);
|
|
2544
|
+
if (isSyncKey && table.source) {
|
|
2545
|
+
runner.all('UPDATE _ganju_tables SET source = ? WHERE id = ?', [
|
|
2546
|
+
JSON.stringify({ ...table.source, key: draft.name }),
|
|
2547
|
+
table.id
|
|
2548
|
+
]);
|
|
2549
|
+
}
|
|
2195
2550
|
}
|
|
2196
2551
|
updateColumnMeta(runner, schema, table, column.id, draft);
|
|
2197
2552
|
touchTable(runner, table.id);
|
|
@@ -2209,6 +2564,9 @@ const dropColumn = (runner, schema, caller, tableName, columnName) => {
|
|
|
2209
2564
|
throw new dataTypes_1.DataError(`${table.name} needs at least one column; drop the table instead.`);
|
|
2210
2565
|
}
|
|
2211
2566
|
assertNoTriggerUses(runner, table, column.name);
|
|
2567
|
+
if (table.source?.key === column.name) {
|
|
2568
|
+
throw new dataTypes_1.DataError(`${table.name}.${column.name} is the key the sheet sync matches rows by; stop syncing the table, or pick another key, first.`, 'constraint');
|
|
2569
|
+
}
|
|
2212
2570
|
if ((0, exports.dropColumnNeedsRebuild)(schema, table.name, column.name)) {
|
|
2213
2571
|
return rebuildTable(runner, schema, caller, table, table.columns.filter(c => c.id !== column.id).map(c => ({ draft: c, id: c.id })), { column: column.name, kind: 'drop' }, column.id);
|
|
2214
2572
|
}
|
package/dist/dataImport.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export interface ImportedRow {
|
|
|
66
66
|
values: Record<string, unknown>;
|
|
67
67
|
errors: string[];
|
|
68
68
|
warnings: NumberGuess[];
|
|
69
|
+
repeated: string[];
|
|
69
70
|
}
|
|
70
71
|
export declare const importRows: (table: Pick<DataTable, "name" | "columns">, mapping: (string | null)[], rows: ImportRow[], options?: {
|
|
71
72
|
relations?: Record<string, RelationLookup>;
|
|
@@ -76,4 +77,6 @@ export declare const importRows: (table: Pick<DataTable, "name" | "columns">, ma
|
|
|
76
77
|
export declare const csvLine: (cells: ImportCell[]) => string;
|
|
77
78
|
export { detectSeparator, parseCsv } from './dataCsv';
|
|
78
79
|
export type { ParsedCsv } from './dataCsv';
|
|
80
|
+
export { DATA_CSV_MIME, DATA_SPREADSHEET_MIME, cellFromSheet, isTabularMimeType, readCsvText, readSheet, readTabularFile } from './dataSheet';
|
|
81
|
+
export type { SheetJs, TabularFile } from './dataSheet';
|
|
79
82
|
//# sourceMappingURL=dataImport.d.ts.map
|
package/dist/dataImport.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataImport.d.ts","sourceRoot":"","sources":["../src/dataImport.ts"],"names":[],"mappings":"AAWA,OAAO,EAGL,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,SAAS,EACf,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGvE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;AACtE,MAAM,MAAM,SAAS,GAAG,UAAU,EAAE,CAAC;AAGrC,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAE3C,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAgCrC,MAAM,MAAM,WAAW,GAAG,GAAG,GAAG,GAAG,CAAC;AAqBpC;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAC5B,MAAM,SAAS,EAAE,KAChB,WAAW,GAAG,SAahB,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IAGd,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GACtB,OAAO,MAAM,EACb,OAAO,WAAW,KACjB,YAAY,GAAG,IAyCjB,CAAC;AAIF,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,WAKhB,CAAC;AAK/B,eAAO,MAAM,oBAAoB,GAAI,QAAQ,MAAM,EAAE,OAAO,MAAM,WAmBjE,CAAC;AAQF,MAAM,WAAW,YAAY;IAE3B,WAAW,CAAC,EAAE,WAAW,CAAC;IAE1B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAGD,eAAO,MAAM,eAAe,GAC1B,QAAQ,UAAU,EAAE,EACpB,UAAS,YAAiB,KACzB;IACD,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CAuFnB,CAAC;AAIF,eAAO,MAAM,YAAY,GACvB,SAAS,MAAM,EAAE,EACjB,MAAM,SAAS,EAAE,EACjB,UAAS;IAAE,WAAW,CAAC,EAAE,WAAW,CAAA;CAAO,KAC1C,eAAe,EAoBjB,CAAC;AAKF,eAAO,MAAM,YAAY,GACvB,SAAS,MAAM,EAAE,EACjB,OAAO,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,KAChC,CAAC,MAAM,GAAG,IAAI,CAAC,EAejB,CAAC;AAKF,MAAM,MAAM,cAAc,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEjD,eAAO,MAAM,WAAW,GAAI,OAAO,UAAU,WAI3B,CAAC;AAInB,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAGD,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,WAAW,CAAA;CAAE,GACnD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAKD,eAAO,MAAM,UAAU,GACrB,OAAO,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,EAC9B,QAAQ,UAAU,EAClB,MAAM,UAAU,EAChB,UAAS,WAAgB,KACxB,UA0EF,CAAC;AAEF,MAAM,WAAW,WAAW;IAE1B,KAAK,EAAE,MAAM,CAAC;IAEd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,EAAE,MAAM,EAAE,CAAC;IAEjB,QAAQ,EAAE,WAAW,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"dataImport.d.ts","sourceRoot":"","sources":["../src/dataImport.ts"],"names":[],"mappings":"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"}
|
package/dist/dataImport.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseCsv = exports.detectSeparator = exports.csvLine = exports.importRows = exports.importCell = exports.relationKey = exports.matchHeaders = exports.guessColumns = exports.guessColumnType = exports.columnNameFromHeader = exports.normaliseHeader = exports.parseNumber = exports.detectDecimalMark = exports.DATA_IMPORT_BATCH = exports.DATA_IMPORT_SAMPLE_ROWS = exports.toTableInput = exports.isValidIdentifier = exports.identifierIssue = void 0;
|
|
3
|
+
exports.readTabularFile = exports.readSheet = exports.readCsvText = exports.isTabularMimeType = exports.cellFromSheet = exports.DATA_SPREADSHEET_MIME = exports.DATA_CSV_MIME = exports.parseCsv = exports.detectSeparator = exports.csvLine = exports.importRows = exports.importCell = exports.relationKey = exports.matchHeaders = exports.guessColumns = exports.guessColumnType = exports.columnNameFromHeader = exports.normaliseHeader = exports.parseNumber = exports.detectDecimalMark = exports.DATA_IMPORT_BATCH = exports.DATA_IMPORT_SAMPLE_ROWS = exports.toTableInput = exports.isValidIdentifier = exports.identifierIssue = void 0;
|
|
4
4
|
// Reading a spreadsheet into a table: the column types guessed for a new
|
|
5
5
|
// table, the headers matched to an existing one's columns, and each cell
|
|
6
6
|
// checked the way the database will check it. The dashboard's import and
|
|
@@ -405,6 +405,7 @@ const importRows = (table, mapping, rows, options = {}) => {
|
|
|
405
405
|
const values = {};
|
|
406
406
|
const errors = missing.map(c => `${c.name} is required.`);
|
|
407
407
|
const warnings = [];
|
|
408
|
+
const repeated = [];
|
|
408
409
|
mapping.forEach((name, i) => {
|
|
409
410
|
if (name === null)
|
|
410
411
|
return;
|
|
@@ -432,11 +433,12 @@ const importRows = (table, mapping, rows, options = {}) => {
|
|
|
432
433
|
if (first === undefined)
|
|
433
434
|
lines.set(key, line);
|
|
434
435
|
else {
|
|
436
|
+
repeated.push(column.name);
|
|
435
437
|
errors.push(`${column.name}: "${text(row[i])}" is already on line ${first}, and this column takes each value once.`);
|
|
436
438
|
}
|
|
437
439
|
}
|
|
438
440
|
});
|
|
439
|
-
return { index, line, values, errors, warnings };
|
|
441
|
+
return { index, line, values, errors, warnings, repeated };
|
|
440
442
|
});
|
|
441
443
|
};
|
|
442
444
|
exports.importRows = importRows;
|
|
@@ -451,3 +453,11 @@ exports.csvLine = csvLine;
|
|
|
451
453
|
var dataCsv_1 = require("./dataCsv");
|
|
452
454
|
Object.defineProperty(exports, "detectSeparator", { enumerable: true, get: function () { return dataCsv_1.detectSeparator; } });
|
|
453
455
|
Object.defineProperty(exports, "parseCsv", { enumerable: true, get: function () { return dataCsv_1.parseCsv; } });
|
|
456
|
+
var dataSheet_1 = require("./dataSheet");
|
|
457
|
+
Object.defineProperty(exports, "DATA_CSV_MIME", { enumerable: true, get: function () { return dataSheet_1.DATA_CSV_MIME; } });
|
|
458
|
+
Object.defineProperty(exports, "DATA_SPREADSHEET_MIME", { enumerable: true, get: function () { return dataSheet_1.DATA_SPREADSHEET_MIME; } });
|
|
459
|
+
Object.defineProperty(exports, "cellFromSheet", { enumerable: true, get: function () { return dataSheet_1.cellFromSheet; } });
|
|
460
|
+
Object.defineProperty(exports, "isTabularMimeType", { enumerable: true, get: function () { return dataSheet_1.isTabularMimeType; } });
|
|
461
|
+
Object.defineProperty(exports, "readCsvText", { enumerable: true, get: function () { return dataSheet_1.readCsvText; } });
|
|
462
|
+
Object.defineProperty(exports, "readSheet", { enumerable: true, get: function () { return dataSheet_1.readSheet; } });
|
|
463
|
+
Object.defineProperty(exports, "readTabularFile", { enumerable: true, get: function () { return dataSheet_1.readTabularFile; } });
|
package/dist/dataRpc.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { DataCaller, DataDeleteImpact, DataFileLinkChange, DataFileLinks, DataFileLinksReplaced, DataResult, DataSchema, DataTable, DataTableCopy } from './dataTypes';
|
|
1
|
+
import type { DataCaller, DataDeleteImpact, DataFileLinkChange, DataFileLinks, DataFileLinksReplaced, DataResult, DataSchema, DataTable, DataTableCopy, DataTableSync } from './dataTypes';
|
|
2
|
+
import type { SheetSyncArgs } from './dataEngine';
|
|
2
3
|
import type { DataQuery } from './dataQueries';
|
|
3
4
|
import type { DataTrigger, DataTriggerWithSecret, SavedTrigger } from './dataTriggers';
|
|
4
5
|
export type ExportFormat = 'jsonl' | 'csv';
|
|
@@ -128,6 +129,13 @@ export interface ProjectDatabaseRpc {
|
|
|
128
129
|
replaceFileLinks(caller: DataCaller, args: {
|
|
129
130
|
changes: DataFileLinkChange[];
|
|
130
131
|
}): R<DataFileLinksReplaced>;
|
|
132
|
+
syncRows(caller: DataCaller, args: SheetSyncArgs): R<DataTableSync>;
|
|
133
|
+
recordSyncFailure(caller: DataCaller, args: {
|
|
134
|
+
table: string;
|
|
135
|
+
error: string;
|
|
136
|
+
title?: string | null;
|
|
137
|
+
version?: string | null;
|
|
138
|
+
}): R<DataTableSync | null>;
|
|
131
139
|
history(caller: DataCaller, args: {
|
|
132
140
|
table: string;
|
|
133
141
|
id: number;
|