latticesql 1.16.0 → 1.16.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.
- package/README.md +9 -1
- package/dist/cli.js +127 -201
- package/dist/index.cjs +10 -0
- package/dist/index.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2300,7 +2300,15 @@ lattice teams join \
|
|
|
2300
2300
|
|
|
2301
2301
|
The cloud rejects redemption if the caller's claimed email doesn't match the invitation's `invitee_email` (case-insensitive). Sharing an invite token in a public channel is therefore safe — only the addressee can redeem it.
|
|
2302
2302
|
|
|
2303
|
-
**Other subcommands** (`lattice teams help` for the full list): `list`, `members`, `leave`, `destroy`, `share`, `unshare`, `shared`, `sync`, `link`, `unlink`, `pull`, `push`, `status`.
|
|
2303
|
+
**Other subcommands** (`lattice teams help` for the full list): `list`, `members`, `leave`, `destroy`, `share`, `unshare`, `shared`, `sync`, `link`, `unlink`, `pull`, `push`, `status`, `dlq`.
|
|
2304
|
+
|
|
2305
|
+
**Dead-letter queue (v1.15+).** A pulled change envelope that fails to apply (e.g. it arrived before the row/table it depends on), and any non-owner-overwrite divergence notice, lands in `__lattice_team_dlq`. Inspect and recover it instead of losing it behind the pull cursor:
|
|
2306
|
+
|
|
2307
|
+
```bash
|
|
2308
|
+
lattice teams dlq list --team <name> # show entries (op, target, error)
|
|
2309
|
+
lattice teams dlq retry --team <name> [--id <id>] # replay; a late dependency now applies cleanly
|
|
2310
|
+
lattice teams dlq purge --team <name> [--id <id>] # discard without applying
|
|
2311
|
+
```
|
|
2304
2312
|
|
|
2305
2313
|
**Per-table ownership + opt-in sharing (v1.14+).** Team members share one physical Postgres, so visibility is enforced at the app layer via a `__lattice_object_owners` table: each table records its creator, and a user sees only the tables they own plus tables explicitly shared to the team. The native `files`/`secrets` objects are owned by the database creator and private by default. Sharing is an explicit, owner-only action (not a side effect of creating a table). The filter gates API access, not just the display.
|
|
2306
2314
|
|
package/dist/cli.js
CHANGED
|
@@ -435,6 +435,15 @@ function resolveDbPath(raw, configDir2) {
|
|
|
435
435
|
}
|
|
436
436
|
return resolve(configDir2, raw);
|
|
437
437
|
}
|
|
438
|
+
var warnedDeprecatedRefs = /* @__PURE__ */ new Set();
|
|
439
|
+
function warnDeprecatedRef(entity, field, target) {
|
|
440
|
+
const key = `${entity}.${field}`;
|
|
441
|
+
if (warnedDeprecatedRefs.has(key)) return;
|
|
442
|
+
warnedDeprecatedRefs.add(key);
|
|
443
|
+
console.warn(
|
|
444
|
+
`Lattice: one-to-many \`ref:\` on "${entity}.${field}" \u2192 "${target}" is deprecated in favor of many-to-many junction tables and will be removed in 2.0.`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
438
447
|
function entityToTableDef(entityName, entity) {
|
|
439
448
|
const rawFields = entity.fields;
|
|
440
449
|
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
|
|
@@ -457,6 +466,7 @@ function entityToTableDef(entityName, entity) {
|
|
|
457
466
|
table: field.ref,
|
|
458
467
|
foreignKey: fieldName
|
|
459
468
|
};
|
|
469
|
+
warnDeprecatedRef(entityName, fieldName, field.ref);
|
|
460
470
|
}
|
|
461
471
|
}
|
|
462
472
|
const primaryKey = entity.primaryKey ?? pkFromField;
|
|
@@ -6238,7 +6248,6 @@ var css = `
|
|
|
6238
6248
|
--danger: #ef4444;
|
|
6239
6249
|
--danger-deep: #dc2626;
|
|
6240
6250
|
--shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
|
|
6241
|
-
--sidebar-width: 380px;
|
|
6242
6251
|
--nav-width: 220px;
|
|
6243
6252
|
}
|
|
6244
6253
|
* { box-sizing: border-box; }
|
|
@@ -6459,30 +6468,11 @@ var css = `
|
|
|
6459
6468
|
minimum keeps the track at content-width and the whole page scrolls
|
|
6460
6469
|
horizontally. */
|
|
6461
6470
|
.layout {
|
|
6462
|
-
display: grid; grid-template-columns: var(--nav-width) minmax(0, 1fr)
|
|
6471
|
+
display: grid; grid-template-columns: var(--nav-width) minmax(0, 1fr);
|
|
6463
6472
|
height: calc(100vh - 56px);
|
|
6464
6473
|
}
|
|
6465
|
-
.rail-handle { display: none; }
|
|
6466
6474
|
@media (max-width: 720px) {
|
|
6467
|
-
|
|
6468
|
-
the feed to ~62svh. */
|
|
6469
|
-
.layout { grid-template-columns: var(--nav-width) minmax(0, 1fr); }
|
|
6470
|
-
.assistant-rail {
|
|
6471
|
-
position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
|
|
6472
|
-
border-left: none; border-top: 1px solid var(--border);
|
|
6473
|
-
max-height: 62svh; box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.4);
|
|
6474
|
-
}
|
|
6475
|
-
.rail-resize { display: none; }
|
|
6476
|
-
.rail-handle {
|
|
6477
|
-
display: block; flex: 0 0 auto; height: 22px; cursor: pointer; position: relative;
|
|
6478
|
-
}
|
|
6479
|
-
.rail-handle::after {
|
|
6480
|
-
content: ''; position: absolute; top: 9px; left: 50%; transform: translateX(-50%);
|
|
6481
|
-
width: 40px; height: 4px; border-radius: 2px; background: var(--border-strong);
|
|
6482
|
-
}
|
|
6483
|
-
.assistant-rail:not(.expanded) { max-height: none; }
|
|
6484
|
-
.assistant-rail:not(.expanded) .rail-feed { display: none; }
|
|
6485
|
-
main#content { padding-bottom: 96px; }
|
|
6475
|
+
main#content { padding-bottom: 24px; }
|
|
6486
6476
|
}
|
|
6487
6477
|
nav.sidebar {
|
|
6488
6478
|
background: var(--surface); border-right: 1px solid var(--border);
|
|
@@ -6506,51 +6496,6 @@ var css = `
|
|
|
6506
6496
|
|
|
6507
6497
|
main#content { padding: 24px; overflow: auto; }
|
|
6508
6498
|
|
|
6509
|
-
/* \u2500\u2500 Assistant rail (activity feed) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6510
|
-
.assistant-rail {
|
|
6511
|
-
position: relative;
|
|
6512
|
-
background: var(--surface);
|
|
6513
|
-
border-left: 1px solid var(--border);
|
|
6514
|
-
display: flex; flex-direction: column;
|
|
6515
|
-
min-width: 0; overflow: hidden;
|
|
6516
|
-
}
|
|
6517
|
-
.rail-resize {
|
|
6518
|
-
position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
|
|
6519
|
-
cursor: col-resize; background: transparent; z-index: 5;
|
|
6520
|
-
transition: background-color 120ms;
|
|
6521
|
-
}
|
|
6522
|
-
.rail-resize:hover, .rail-resize.dragging { background: var(--accent-soft); }
|
|
6523
|
-
.rail-header {
|
|
6524
|
-
flex: 0 0 auto; padding: 12px 14px; border-bottom: 1px solid var(--border);
|
|
6525
|
-
display: flex; align-items: center; gap: 8px;
|
|
6526
|
-
}
|
|
6527
|
-
.rail-title {
|
|
6528
|
-
font-size: 11px; font-weight: 600; color: var(--text-muted);
|
|
6529
|
-
text-transform: uppercase; letter-spacing: 0.06em; flex: 0 0 auto;
|
|
6530
|
-
}
|
|
6531
|
-
.rail-feed {
|
|
6532
|
-
flex: 1 1 auto; overflow-y: auto; padding: 10px 12px;
|
|
6533
|
-
display: flex; flex-direction: column; gap: 8px;
|
|
6534
|
-
}
|
|
6535
|
-
.rail-empty { color: var(--text-muted); font-size: 12.5px; text-align: center; padding: 18px 8px; }
|
|
6536
|
-
.feed-item {
|
|
6537
|
-
display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; gap: 8px;
|
|
6538
|
-
align-items: baseline; padding: 7px 9px; border-radius: 8px;
|
|
6539
|
-
background: var(--surface-2); border: 1px solid var(--border);
|
|
6540
|
-
animation: feedIn 0.18s ease-out;
|
|
6541
|
-
}
|
|
6542
|
-
@keyframes feedIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
|
6543
|
-
.feed-icon { text-align: center; font-size: 13px; }
|
|
6544
|
-
.feed-body { min-width: 0; }
|
|
6545
|
-
.feed-summary { font-size: 13px; color: var(--text); word-break: break-word; }
|
|
6546
|
-
.feed-meta { margin-top: 2px; display: flex; align-items: center; gap: 6px; }
|
|
6547
|
-
.feed-source {
|
|
6548
|
-
font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
|
|
6549
|
-
padding: 1px 6px; border-radius: 999px;
|
|
6550
|
-
background: var(--accent-soft); color: var(--accent);
|
|
6551
|
-
}
|
|
6552
|
-
.feed-time { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
|
|
6553
|
-
|
|
6554
6499
|
/* \u2500\u2500 File preview (files detail page) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6555
6500
|
.file-preview { margin: 4px 0 16px; }
|
|
6556
6501
|
.file-preview .file-desc {
|
|
@@ -7412,9 +7357,6 @@ var appJs = `
|
|
|
7412
7357
|
refreshHistoryState();
|
|
7413
7358
|
renderRoute();
|
|
7414
7359
|
startRealtime();
|
|
7415
|
-
initRailResize();
|
|
7416
|
-
initRailDrawer();
|
|
7417
|
-
startFeed();
|
|
7418
7360
|
initSearch();
|
|
7419
7361
|
initLastEdited();
|
|
7420
7362
|
initOffline();
|
|
@@ -7770,11 +7712,12 @@ var appJs = `
|
|
|
7770
7712
|
}
|
|
7771
7713
|
|
|
7772
7714
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7773
|
-
//
|
|
7774
|
-
//
|
|
7775
|
-
//
|
|
7715
|
+
// Shared activity helpers \u2014 the operation-icon map and relative-time
|
|
7716
|
+
// formatter, used by Version History and the dashboard activity list. The
|
|
7717
|
+
// standalone Activity rail was removed in 1.16.1 (redundant with Version
|
|
7718
|
+
// History); multiplayer realtime convergence runs on the separate realtime
|
|
7719
|
+
// channel (startRealtime), not on this.
|
|
7776
7720
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7777
|
-
var feedSource = null;
|
|
7778
7721
|
var FEED_ICONS = {
|
|
7779
7722
|
insert: '\u2795', update: '\u270F\uFE0F', delete: '\u{1F5D1}',
|
|
7780
7723
|
link: '\u{1F517}', unlink: '\u26D3', undo: '\u21B6', redo: '\u21B7', schema: '\u{1F6E0}',
|
|
@@ -7790,51 +7733,6 @@ var appJs = `
|
|
|
7790
7733
|
return new Date(iso).toLocaleDateString();
|
|
7791
7734
|
} catch (_) { return ''; }
|
|
7792
7735
|
}
|
|
7793
|
-
function renderFeedItem(ev) {
|
|
7794
|
-
var feedEl = document.getElementById('rail-feed');
|
|
7795
|
-
if (!feedEl) return;
|
|
7796
|
-
var empty = document.getElementById('rail-empty');
|
|
7797
|
-
if (empty) empty.remove();
|
|
7798
|
-
var item = document.createElement('div');
|
|
7799
|
-
item.className = 'feed-item';
|
|
7800
|
-
var icon = document.createElement('div');
|
|
7801
|
-
icon.className = 'feed-icon';
|
|
7802
|
-
icon.textContent = FEED_ICONS[ev.op] || '\u2022';
|
|
7803
|
-
var body = document.createElement('div');
|
|
7804
|
-
body.className = 'feed-body';
|
|
7805
|
-
var summary = document.createElement('div');
|
|
7806
|
-
summary.className = 'feed-summary';
|
|
7807
|
-
summary.textContent = ev.summary || (String(ev.op || '') + ' ' + String(ev.table || ''));
|
|
7808
|
-
var meta = document.createElement('div');
|
|
7809
|
-
meta.className = 'feed-meta';
|
|
7810
|
-
var src = document.createElement('span');
|
|
7811
|
-
src.className = 'feed-source';
|
|
7812
|
-
src.textContent = ev.source === 'gui' ? 'you' : String(ev.source || '');
|
|
7813
|
-
meta.appendChild(src);
|
|
7814
|
-
body.appendChild(summary);
|
|
7815
|
-
body.appendChild(meta);
|
|
7816
|
-
var time = document.createElement('div');
|
|
7817
|
-
time.className = 'feed-time';
|
|
7818
|
-
time.textContent = relTime(ev.ts);
|
|
7819
|
-
item.appendChild(icon);
|
|
7820
|
-
item.appendChild(body);
|
|
7821
|
-
item.appendChild(time);
|
|
7822
|
-
// Most-recent on top: prepend new items and keep the view scrolled up.
|
|
7823
|
-
feedEl.insertBefore(item, feedEl.firstChild);
|
|
7824
|
-
feedEl.scrollTop = 0;
|
|
7825
|
-
}
|
|
7826
|
-
function startFeed() {
|
|
7827
|
-
if (feedSource) {
|
|
7828
|
-
try { feedSource.close(); } catch (_) { /* ignore */ }
|
|
7829
|
-
feedSource = null;
|
|
7830
|
-
}
|
|
7831
|
-
if (typeof EventSource === 'undefined') return;
|
|
7832
|
-
feedSource = new EventSource('/api/feed/stream');
|
|
7833
|
-
feedSource.addEventListener('feed', function (ev) {
|
|
7834
|
-
try { renderFeedItem(JSON.parse(ev.data)); } catch (_) { /* ignore malformed */ }
|
|
7835
|
-
});
|
|
7836
|
-
// EventSource auto-reconnects on error; no extra handling needed.
|
|
7837
|
-
}
|
|
7838
7736
|
|
|
7839
7737
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7840
7738
|
// Full-text search \u2014 GET /api/search, grouped dropdown, click to open.
|
|
@@ -7916,52 +7814,6 @@ var appJs = `
|
|
|
7916
7814
|
});
|
|
7917
7815
|
}
|
|
7918
7816
|
|
|
7919
|
-
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7920
|
-
// Activity rail resize \u2014 drag the left edge, clamp, persist.
|
|
7921
|
-
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7922
|
-
var RAIL_MIN = 320, RAIL_MAX = 640, RAIL_KEY = 'lattice-rail-width';
|
|
7923
|
-
function applyRailWidth(px) {
|
|
7924
|
-
var w = Math.min(RAIL_MAX, Math.max(RAIL_MIN, Math.round(px)));
|
|
7925
|
-
document.documentElement.style.setProperty('--sidebar-width', w + 'px');
|
|
7926
|
-
return w;
|
|
7927
|
-
}
|
|
7928
|
-
function initRailResize() {
|
|
7929
|
-
var saved = parseInt(window.localStorage.getItem(RAIL_KEY) || '', 10);
|
|
7930
|
-
if (!isNaN(saved)) applyRailWidth(saved);
|
|
7931
|
-
var handle = document.getElementById('rail-resize');
|
|
7932
|
-
if (!handle) return;
|
|
7933
|
-
handle.addEventListener('pointerdown', function (e) {
|
|
7934
|
-
e.preventDefault();
|
|
7935
|
-
var startX = e.clientX;
|
|
7936
|
-
var rail = document.getElementById('assistant-rail');
|
|
7937
|
-
var startW = rail ? rail.getBoundingClientRect().width : 380;
|
|
7938
|
-
handle.classList.add('dragging');
|
|
7939
|
-
function move(ev) {
|
|
7940
|
-
// Rail sits on the right; dragging left (smaller clientX) widens it.
|
|
7941
|
-
applyRailWidth(startW - (ev.clientX - startX));
|
|
7942
|
-
}
|
|
7943
|
-
function up() {
|
|
7944
|
-
handle.classList.remove('dragging');
|
|
7945
|
-
window.removeEventListener('pointermove', move);
|
|
7946
|
-
window.removeEventListener('pointerup', up);
|
|
7947
|
-
var cur = parseInt(
|
|
7948
|
-
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width'),
|
|
7949
|
-
10,
|
|
7950
|
-
);
|
|
7951
|
-
if (!isNaN(cur)) window.localStorage.setItem(RAIL_KEY, String(cur));
|
|
7952
|
-
}
|
|
7953
|
-
window.addEventListener('pointermove', move);
|
|
7954
|
-
window.addEventListener('pointerup', up);
|
|
7955
|
-
});
|
|
7956
|
-
}
|
|
7957
|
-
|
|
7958
|
-
// Mobile: tapping the handle expands/collapses the bottom drawer.
|
|
7959
|
-
function initRailDrawer() {
|
|
7960
|
-
var handle = document.getElementById('rail-handle');
|
|
7961
|
-
var rail = document.getElementById('assistant-rail');
|
|
7962
|
-
if (handle && rail) handle.addEventListener('click', function () { rail.classList.toggle('expanded'); });
|
|
7963
|
-
}
|
|
7964
|
-
|
|
7965
7817
|
/** Reload column meta after a secret-flag change. */
|
|
7966
7818
|
function refreshColumnMeta() {
|
|
7967
7819
|
return fetchJson('/api/gui-meta/columns').then(function (d) {
|
|
@@ -8112,10 +7964,10 @@ var appJs = `
|
|
|
8112
7964
|
else renderRoute();
|
|
8113
7965
|
loadedTables = {};
|
|
8114
7966
|
startRealtime();
|
|
8115
|
-
startFeed();
|
|
8116
7967
|
});
|
|
8117
7968
|
}
|
|
8118
7969
|
|
|
7970
|
+
var wsOutsideClickBound = false;
|
|
8119
7971
|
function renderWsSwitcher(data) {
|
|
8120
7972
|
var wrap = document.getElementById('ws-switcher');
|
|
8121
7973
|
var btn = document.getElementById('ws-button');
|
|
@@ -8173,8 +8025,13 @@ var appJs = `
|
|
|
8173
8025
|
});
|
|
8174
8026
|
});
|
|
8175
8027
|
});
|
|
8176
|
-
document.getElementById('ws-create-btn').addEventListener('click', function () {
|
|
8177
|
-
showCreateWorkspaceInput
|
|
8028
|
+
document.getElementById('ws-create-btn').addEventListener('click', function (e) {
|
|
8029
|
+
// Stop propagation: showCreateWorkspaceInput replaces .db-create's
|
|
8030
|
+
// innerHTML, detaching THIS button. Without this, the click then
|
|
8031
|
+
// bubbles to the document outside-click closer, whose
|
|
8032
|
+
// menu.contains(e.target) is now false (target detached) \u2192 it would
|
|
8033
|
+
// close the menu, so the create input never appears.
|
|
8034
|
+
e.stopPropagation(); showCreateWorkspaceInput(menu);
|
|
8178
8035
|
});
|
|
8179
8036
|
}
|
|
8180
8037
|
|
|
@@ -8183,12 +8040,20 @@ var appJs = `
|
|
|
8183
8040
|
if (menu.hidden) buildMenu();
|
|
8184
8041
|
menu.hidden = !menu.hidden;
|
|
8185
8042
|
};
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8043
|
+
// Attach the outside-click closer ONCE \u2014 renderWsSwitcher runs on every
|
|
8044
|
+
// reload, so adding it each time leaked a listener per render. Re-fetch
|
|
8045
|
+
// the elements by id inside the handler so it never holds a stale closure.
|
|
8046
|
+
if (!wsOutsideClickBound) {
|
|
8047
|
+
wsOutsideClickBound = true;
|
|
8048
|
+
document.addEventListener('click', function (e) {
|
|
8049
|
+
var m = document.getElementById('ws-menu');
|
|
8050
|
+
var b = document.getElementById('ws-button');
|
|
8051
|
+
if (!m || m.hidden) return;
|
|
8052
|
+
if (!m.contains(e.target) && e.target !== b && (!b || !b.contains(e.target))) {
|
|
8053
|
+
m.hidden = true;
|
|
8054
|
+
}
|
|
8055
|
+
});
|
|
8056
|
+
}
|
|
8192
8057
|
}
|
|
8193
8058
|
|
|
8194
8059
|
// Inline "new workspace" name entry, shown inside the Workspaces menu.
|
|
@@ -10288,7 +10153,9 @@ var appJs = `
|
|
|
10288
10153
|
var linkRows = dmLinks.map(function (lk, i) {
|
|
10289
10154
|
return '<div class="dm-link-row">' +
|
|
10290
10155
|
'<span class="dm-link-name">' + escapeHtml(displayFor(lk.other).label) + '</span>' +
|
|
10291
|
-
'<span class="dm-link-arrow"
|
|
10156
|
+
'<span class="dm-link-arrow' + (lk.kind === 'fk' ? ' legacy' : '') + '" ' +
|
|
10157
|
+
(lk.kind === 'fk' ? 'title="Legacy one-to-many link. New links are many-to-many; this is kept for back-compat and will be migrated in 2.0."' : '') +
|
|
10158
|
+
'>' + (lk.kind === 'fk' ? '\u2192 one-to-many (legacy)' : '\u2194 many-to-many') + '</span>' +
|
|
10292
10159
|
'<button class="btn danger dm-link-destroy" data-link="' + i +
|
|
10293
10160
|
'" title="Delete this link \u2014 removes it from both tables">Delete link</button>' +
|
|
10294
10161
|
'</div>';
|
|
@@ -11569,11 +11436,11 @@ var appJs = `
|
|
|
11569
11436
|
return (
|
|
11570
11437
|
'<p style="margin:0 0 12px;color:var(--text-muted);font-size:13px">' +
|
|
11571
11438
|
'SQLite DB: <code>' + escapeHtml(info.dbFile || '(unknown)') + '</code>. ' +
|
|
11572
|
-
'
|
|
11439
|
+
'Push this workspace to a cloud Postgres to collaborate. ' +
|
|
11440
|
+
'(To join an existing cloud, create a new workspace and choose \u201Cjoin via cloud invite\u201D.)' +
|
|
11573
11441
|
'</p>' +
|
|
11574
11442
|
'<div class="team-actions">' +
|
|
11575
11443
|
'<button class="btn primary" data-act="open-migrate">Migrate to cloud \u2192</button>' +
|
|
11576
|
-
'<button class="btn" data-act="open-connect-existing">Connect to existing cloud \u2192</button>' +
|
|
11577
11444
|
'</div>'
|
|
11578
11445
|
);
|
|
11579
11446
|
}
|
|
@@ -11642,11 +11509,6 @@ var appJs = `
|
|
|
11642
11509
|
showMigrateToCloudModal(rerender);
|
|
11643
11510
|
});
|
|
11644
11511
|
|
|
11645
|
-
var connectExBtn = host.querySelector('[data-act="open-connect-existing"]');
|
|
11646
|
-
if (connectExBtn) connectExBtn.addEventListener('click', function () {
|
|
11647
|
-
showConnectExistingModal(rerender);
|
|
11648
|
-
});
|
|
11649
|
-
|
|
11650
11512
|
var upgradeBtn = host.querySelector('[data-act="open-upgrade"]');
|
|
11651
11513
|
if (upgradeBtn) upgradeBtn.addEventListener('click', function () {
|
|
11652
11514
|
showUpgradeToTeamModal(rerender);
|
|
@@ -12218,16 +12080,6 @@ var guiAppHtml = `<!doctype html>
|
|
|
12218
12080
|
</div>
|
|
12219
12081
|
</nav>
|
|
12220
12082
|
<main id="content"></main>
|
|
12221
|
-
<aside class="assistant-rail" id="assistant-rail">
|
|
12222
|
-
<div class="rail-resize" id="rail-resize" role="separator" aria-orientation="vertical" title="Drag to resize"></div>
|
|
12223
|
-
<div class="rail-handle" id="rail-handle" title="Expand / collapse"></div>
|
|
12224
|
-
<div class="rail-header">
|
|
12225
|
-
<span class="rail-title">Activity</span>
|
|
12226
|
-
</div>
|
|
12227
|
-
<div class="rail-feed" id="rail-feed">
|
|
12228
|
-
<div class="rail-empty" id="rail-empty">No activity yet. Changes you make will appear here.</div>
|
|
12229
|
-
</div>
|
|
12230
|
-
</aside>
|
|
12231
12083
|
</div>
|
|
12232
12084
|
|
|
12233
12085
|
<div class="drawer-backdrop" id="drawer-backdrop" hidden></div>
|
|
@@ -13653,10 +13505,32 @@ async function createRow(ctx, table, values) {
|
|
|
13653
13505
|
await emitTeamEnvelope(ctx, table, id, "upsert", row);
|
|
13654
13506
|
return { id, row };
|
|
13655
13507
|
}
|
|
13508
|
+
function storedValueMatches(stored, requested) {
|
|
13509
|
+
if (stored === requested) return true;
|
|
13510
|
+
const storedEmpty = stored === null || stored === void 0 || stored === "";
|
|
13511
|
+
const reqEmpty = requested === null || requested === void 0 || requested === "";
|
|
13512
|
+
if (storedEmpty && reqEmpty) return true;
|
|
13513
|
+
if (typeof requested === "boolean") return Number(stored) === Number(requested);
|
|
13514
|
+
if (typeof requested === "number") return Number(stored) === requested;
|
|
13515
|
+
return String(stored) === String(requested);
|
|
13516
|
+
}
|
|
13517
|
+
function rowsEqual(a, b) {
|
|
13518
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
13519
|
+
for (const k of keys) if (a[k] !== b[k]) return false;
|
|
13520
|
+
return true;
|
|
13521
|
+
}
|
|
13656
13522
|
async function updateRow(ctx, table, id, values) {
|
|
13657
13523
|
const before = await ctx.db.get(table, id);
|
|
13658
13524
|
await ctx.db.update(table, id, values);
|
|
13659
13525
|
const after = await ctx.db.get(table, id);
|
|
13526
|
+
if (before != null && after != null) {
|
|
13527
|
+
const wantedChange = Object.keys(values).some(
|
|
13528
|
+
(k) => !storedValueMatches(before[k], values[k])
|
|
13529
|
+
);
|
|
13530
|
+
if (wantedChange && rowsEqual(before, after)) {
|
|
13531
|
+
throw new Error("Row update did not persist \u2014 the data source may be read-only");
|
|
13532
|
+
}
|
|
13533
|
+
}
|
|
13660
13534
|
await appendAudit(
|
|
13661
13535
|
ctx.db,
|
|
13662
13536
|
ctx.feed,
|
|
@@ -18194,6 +18068,14 @@ data: ${JSON.stringify(data)}
|
|
|
18194
18068
|
sendJson(res, { error: `Unknown entity: ${oldName}` }, 400);
|
|
18195
18069
|
return;
|
|
18196
18070
|
}
|
|
18071
|
+
if (isNativeEntity(oldName)) {
|
|
18072
|
+
sendJson(
|
|
18073
|
+
res,
|
|
18074
|
+
{ error: `"${oldName}" is a built-in entity and cannot be modified` },
|
|
18075
|
+
400
|
|
18076
|
+
);
|
|
18077
|
+
return;
|
|
18078
|
+
}
|
|
18197
18079
|
if (!operatorOwnsTable(active.teamContext, oldName)) {
|
|
18198
18080
|
sendJson(res, { error: "Only the table owner can edit this entity" }, 403);
|
|
18199
18081
|
return;
|
|
@@ -18239,6 +18121,14 @@ data: ${JSON.stringify(data)}
|
|
|
18239
18121
|
sendJson(res, { error: `Unknown entity: ${entityName}` }, 400);
|
|
18240
18122
|
return;
|
|
18241
18123
|
}
|
|
18124
|
+
if (isNativeEntity(entityName)) {
|
|
18125
|
+
sendJson(
|
|
18126
|
+
res,
|
|
18127
|
+
{ error: `"${entityName}" is a built-in entity and cannot be modified` },
|
|
18128
|
+
400
|
|
18129
|
+
);
|
|
18130
|
+
return;
|
|
18131
|
+
}
|
|
18242
18132
|
if (!operatorOwnsTable(active.teamContext, entityName)) {
|
|
18243
18133
|
sendJson(res, { error: "Only the table owner can edit this entity" }, 403);
|
|
18244
18134
|
return;
|
|
@@ -18266,12 +18156,22 @@ data: ${JSON.stringify(data)}
|
|
|
18266
18156
|
sendJson(res, { error: "Use \u201CAdd link\u201D to create a relationship column" }, 400);
|
|
18267
18157
|
return;
|
|
18268
18158
|
}
|
|
18159
|
+
const doc = loadConfigDoc(active.configPath);
|
|
18160
|
+
const fieldsNode = doc.getIn(["entities", entityName, "fields"]);
|
|
18161
|
+
if (!fieldsNode || typeof fieldsNode !== "object" || typeof fieldsNode.toJSON !== "function") {
|
|
18162
|
+
sendJson(res, { error: `Cannot add columns to "${entityName}"` }, 400);
|
|
18163
|
+
return;
|
|
18164
|
+
}
|
|
18165
|
+
const existingFields = fieldsNode.toJSON();
|
|
18166
|
+
if (colName in existingFields) {
|
|
18167
|
+
sendJson(res, { error: `Column "${colName}" already exists on ${entityName}` }, 400);
|
|
18168
|
+
return;
|
|
18169
|
+
}
|
|
18269
18170
|
const sqliteType = fieldToSqliteBaseType(colType);
|
|
18270
18171
|
await execSql(
|
|
18271
18172
|
active.db,
|
|
18272
18173
|
`ALTER TABLE "${entityName}" ADD COLUMN "${colName}" ${sqliteType}`
|
|
18273
18174
|
);
|
|
18274
|
-
const doc = loadConfigDoc(active.configPath);
|
|
18275
18175
|
const fieldDef = { type: colType };
|
|
18276
18176
|
if (body.required === true) fieldDef.required = true;
|
|
18277
18177
|
doc.setIn(["entities", entityName, "fields", colName], fieldDef);
|
|
@@ -18298,6 +18198,14 @@ data: ${JSON.stringify(data)}
|
|
|
18298
18198
|
sendJson(res, { error: `Unknown entity: ${entityName}` }, 400);
|
|
18299
18199
|
return;
|
|
18300
18200
|
}
|
|
18201
|
+
if (isNativeEntity(entityName)) {
|
|
18202
|
+
sendJson(
|
|
18203
|
+
res,
|
|
18204
|
+
{ error: `"${entityName}" is a built-in entity and cannot be modified` },
|
|
18205
|
+
400
|
|
18206
|
+
);
|
|
18207
|
+
return;
|
|
18208
|
+
}
|
|
18301
18209
|
if (!operatorOwnsTable(active.teamContext, entityName)) {
|
|
18302
18210
|
sendJson(res, { error: "Only the table owner can edit this entity" }, 403);
|
|
18303
18211
|
return;
|
|
@@ -18320,14 +18228,30 @@ data: ${JSON.stringify(data)}
|
|
|
18320
18228
|
sendJson(res, { error: `"${newCol}" is a reserved system column` }, 400);
|
|
18321
18229
|
return;
|
|
18322
18230
|
}
|
|
18231
|
+
const doc = loadConfigDoc(active.configPath);
|
|
18232
|
+
const fieldsNode = doc.getIn(["entities", entityName, "fields"]);
|
|
18233
|
+
if (!fieldsNode || typeof fieldsNode !== "object" || typeof fieldsNode.toJSON !== "function") {
|
|
18234
|
+
sendJson(res, { error: `Cannot rename columns on "${entityName}"` }, 400);
|
|
18235
|
+
return;
|
|
18236
|
+
}
|
|
18237
|
+
const fieldsObj = fieldsNode.toJSON();
|
|
18238
|
+
if (!(colName in fieldsObj)) {
|
|
18239
|
+
sendJson(res, { error: `Unknown column "${colName}" on ${entityName}` }, 400);
|
|
18240
|
+
return;
|
|
18241
|
+
}
|
|
18242
|
+
if (newCol in fieldsObj) {
|
|
18243
|
+
sendJson(res, { error: `Column "${newCol}" already exists on ${entityName}` }, 400);
|
|
18244
|
+
return;
|
|
18245
|
+
}
|
|
18323
18246
|
await execSql(
|
|
18324
18247
|
active.db,
|
|
18325
18248
|
`ALTER TABLE "${entityName}" RENAME COLUMN "${colName}" TO "${newCol}"`
|
|
18326
18249
|
);
|
|
18327
|
-
const
|
|
18328
|
-
const
|
|
18329
|
-
|
|
18330
|
-
|
|
18250
|
+
const renamedFields = {};
|
|
18251
|
+
for (const k of Object.keys(fieldsObj)) {
|
|
18252
|
+
renamedFields[k === colName ? newCol : k] = fieldsObj[k];
|
|
18253
|
+
}
|
|
18254
|
+
doc.setIn(["entities", entityName, "fields"], renamedFields);
|
|
18331
18255
|
saveConfigDoc(active.configPath, doc);
|
|
18332
18256
|
await disposeActive(active);
|
|
18333
18257
|
active = await openConfig(active.configPath, active.outputDir, autoRender);
|
|
@@ -18569,10 +18493,12 @@ data: ${JSON.stringify(data)}
|
|
|
18569
18493
|
(e) => e.table_name === filterTable || junctionMatchesFilter.has(e.table_name)
|
|
18570
18494
|
);
|
|
18571
18495
|
}
|
|
18572
|
-
const
|
|
18573
|
-
|
|
18574
|
-
|
|
18575
|
-
|
|
18496
|
+
const sessionRows = await active.db.query("_lattice_gui_audit", {
|
|
18497
|
+
filters: [{ col: "session_id", op: "eq", val: sessionId }]
|
|
18498
|
+
});
|
|
18499
|
+
const sessionLive = sessionRows.filter((r) => Number(r.undone) === 0).length;
|
|
18500
|
+
const sessionUndone = sessionRows.length - sessionLive;
|
|
18501
|
+
sendJson(res, { entries, canUndo: sessionLive > 0, canRedo: sessionUndone > 0 });
|
|
18576
18502
|
return;
|
|
18577
18503
|
}
|
|
18578
18504
|
if (method === "POST" && pathname === "/api/history/undo") {
|
package/dist/index.cjs
CHANGED
|
@@ -3560,6 +3560,15 @@ function resolveDbPath(raw, configDir2) {
|
|
|
3560
3560
|
}
|
|
3561
3561
|
return (0, import_node_path11.resolve)(configDir2, raw);
|
|
3562
3562
|
}
|
|
3563
|
+
var warnedDeprecatedRefs = /* @__PURE__ */ new Set();
|
|
3564
|
+
function warnDeprecatedRef(entity, field, target) {
|
|
3565
|
+
const key = `${entity}.${field}`;
|
|
3566
|
+
if (warnedDeprecatedRefs.has(key)) return;
|
|
3567
|
+
warnedDeprecatedRefs.add(key);
|
|
3568
|
+
console.warn(
|
|
3569
|
+
`Lattice: one-to-many \`ref:\` on "${entity}.${field}" \u2192 "${target}" is deprecated in favor of many-to-many junction tables and will be removed in 2.0.`
|
|
3570
|
+
);
|
|
3571
|
+
}
|
|
3563
3572
|
function entityToTableDef(entityName, entity) {
|
|
3564
3573
|
const rawFields = entity.fields;
|
|
3565
3574
|
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
|
|
@@ -3582,6 +3591,7 @@ function entityToTableDef(entityName, entity) {
|
|
|
3582
3591
|
table: field.ref,
|
|
3583
3592
|
foreignKey: fieldName
|
|
3584
3593
|
};
|
|
3594
|
+
warnDeprecatedRef(entityName, fieldName, field.ref);
|
|
3585
3595
|
}
|
|
3586
3596
|
}
|
|
3587
3597
|
const primaryKey = entity.primaryKey ?? pkFromField;
|
package/dist/index.js
CHANGED
|
@@ -3436,6 +3436,15 @@ function resolveDbPath(raw, configDir2) {
|
|
|
3436
3436
|
}
|
|
3437
3437
|
return resolve2(configDir2, raw);
|
|
3438
3438
|
}
|
|
3439
|
+
var warnedDeprecatedRefs = /* @__PURE__ */ new Set();
|
|
3440
|
+
function warnDeprecatedRef(entity, field, target) {
|
|
3441
|
+
const key = `${entity}.${field}`;
|
|
3442
|
+
if (warnedDeprecatedRefs.has(key)) return;
|
|
3443
|
+
warnedDeprecatedRefs.add(key);
|
|
3444
|
+
console.warn(
|
|
3445
|
+
`Lattice: one-to-many \`ref:\` on "${entity}.${field}" \u2192 "${target}" is deprecated in favor of many-to-many junction tables and will be removed in 2.0.`
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3439
3448
|
function entityToTableDef(entityName, entity) {
|
|
3440
3449
|
const rawFields = entity.fields;
|
|
3441
3450
|
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
|
|
@@ -3458,6 +3467,7 @@ function entityToTableDef(entityName, entity) {
|
|
|
3458
3467
|
table: field.ref,
|
|
3459
3468
|
foreignKey: fieldName
|
|
3460
3469
|
};
|
|
3470
|
+
warnDeprecatedRef(entityName, fieldName, field.ref);
|
|
3461
3471
|
}
|
|
3462
3472
|
}
|
|
3463
3473
|
const primaryKey = entity.primaryKey ?? pkFromField;
|
package/package.json
CHANGED