latticesql 1.13.4 → 1.13.6
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/cli.js +426 -43
- package/dist/index.cjs +354 -4
- package/dist/index.d.cts +15 -2
- package/dist/index.d.ts +15 -2
- package/dist/index.js +353 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
10
|
-
import { resolve as resolve9, dirname as
|
|
10
|
+
import { resolve as resolve9, dirname as dirname8 } from "path";
|
|
11
11
|
import { readFileSync as readFileSync12 } from "fs";
|
|
12
12
|
import { execSync } from "child_process";
|
|
13
13
|
import { parse as parse2 } from "yaml";
|
|
@@ -189,6 +189,23 @@ function saveDbCredential(label, url) {
|
|
|
189
189
|
creds[label] = url;
|
|
190
190
|
saveCredentials(creds);
|
|
191
191
|
}
|
|
192
|
+
function sanitizeTeamLabel(teamName) {
|
|
193
|
+
const stripped = teamName.trim().replace(/\s+/g, "-").replace(/[^A-Za-z0-9._-]+/g, "");
|
|
194
|
+
if (stripped.length === 0) return "team";
|
|
195
|
+
return stripped.startsWith(".") ? `team-${stripped}` : stripped;
|
|
196
|
+
}
|
|
197
|
+
function saveDbCredentialForTeam(opts) {
|
|
198
|
+
const base = sanitizeTeamLabel(opts.teamName);
|
|
199
|
+
let label = `${base}.config`;
|
|
200
|
+
const creds = loadCredentials();
|
|
201
|
+
if (label in creds && creds[label] !== opts.cloudUrl) {
|
|
202
|
+
const shortId = opts.teamId.split("-")[0] ?? opts.teamId.slice(0, 8);
|
|
203
|
+
label = `${base}-${shortId}.config`;
|
|
204
|
+
}
|
|
205
|
+
creds[label] = opts.cloudUrl;
|
|
206
|
+
saveCredentials(creds);
|
|
207
|
+
return label;
|
|
208
|
+
}
|
|
192
209
|
var KEYS_SUBDIR = "keys";
|
|
193
210
|
var TOKEN_EXT = ".token";
|
|
194
211
|
function ensureKeysDir() {
|
|
@@ -4962,8 +4979,8 @@ async function checkForUpdate(pkgName, currentVersion) {
|
|
|
4962
4979
|
// src/gui/server.ts
|
|
4963
4980
|
import { createServer } from "http";
|
|
4964
4981
|
import { spawn } from "child_process";
|
|
4965
|
-
import { existsSync as
|
|
4966
|
-
import { basename as basename6, dirname as
|
|
4982
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync11, readdirSync as readdirSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
4983
|
+
import { basename as basename6, dirname as dirname7, join as join14, resolve as resolve6, sep as sep3 } from "path";
|
|
4967
4984
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
4968
4985
|
|
|
4969
4986
|
// src/gui/data.ts
|
|
@@ -7613,8 +7630,10 @@ var guiAppHtml = `<!doctype html>
|
|
|
7613
7630
|
// enters per-team things (cloud URL + team name) in this modal.
|
|
7614
7631
|
fetchJson('/api/userconfig/identity').then(function (id) {
|
|
7615
7632
|
var bodyHtml =
|
|
7616
|
-
'<div class="field"><label>Cloud URL</label
|
|
7617
|
-
|
|
7633
|
+
'<div class="field"><label>Cloud URL</label>' +
|
|
7634
|
+
'<input name="cloud_url" placeholder="postgres://postgres.<ref>:password@aws-x-region.pooler.supabase.com:5432/postgres" autocapitalize="off" autocorrect="off" spellcheck="false" />' +
|
|
7635
|
+
'</div>' +
|
|
7636
|
+
'<div class="field"><label>Your email</label><input name="email" value="' + escapeHtml(id.email || '') + '" autocapitalize="off" /></div>' +
|
|
7618
7637
|
'<div class="field"><label>Your display name</label><input name="user_name" value="' + escapeHtml(id.display_name || '') + '" /></div>' +
|
|
7619
7638
|
'<div class="field"><label>Team name</label><input name="team_name" /></div>' +
|
|
7620
7639
|
'<p style="font-size:12px;color:var(--text-muted);margin:0">' +
|
|
@@ -7638,12 +7657,14 @@ var guiAppHtml = `<!doctype html>
|
|
|
7638
7657
|
function showJoinTeamModal(kind) {
|
|
7639
7658
|
fetchJson('/api/userconfig/identity').then(function (id) {
|
|
7640
7659
|
var bodyHtml =
|
|
7641
|
-
'<div class="field"><label>Cloud URL</label
|
|
7642
|
-
|
|
7643
|
-
'
|
|
7660
|
+
'<div class="field"><label>Cloud URL</label>' +
|
|
7661
|
+
'<input name="cloud_url" placeholder="postgres://postgres.<ref>:password@aws-x-region.pooler.supabase.com:5432/postgres" autocapitalize="off" autocorrect="off" spellcheck="false" />' +
|
|
7662
|
+
'</div>' +
|
|
7663
|
+
'<div class="field"><label>Invite token</label><textarea name="invite_token" placeholder="latinv_..." autocapitalize="off" autocorrect="off" spellcheck="false"></textarea></div>' +
|
|
7664
|
+
'<div class="field"><label>Your email</label><input name="email" value="' + escapeHtml(id.email || '') + '" autocapitalize="off" /></div>' +
|
|
7644
7665
|
'<div class="field"><label>Your display name</label><input name="name" value="' + escapeHtml(id.display_name || '') + '" /></div>' +
|
|
7645
7666
|
'<p style="font-size:12px;color:var(--text-muted);margin:0">' +
|
|
7646
|
-
'Email must match the address the invitation was addressed to.' +
|
|
7667
|
+
'Use the same Postgres URL the inviter used (postgres://\u2026). Email must match the address the invitation was addressed to.' +
|
|
7647
7668
|
'</p>';
|
|
7648
7669
|
showModal('Join team', bodyHtml, {
|
|
7649
7670
|
primaryLabel: 'Join',
|
|
@@ -9926,7 +9947,7 @@ async function handleDeleteRow(res, ctx, teamId, tableName, pk) {
|
|
|
9926
9947
|
}
|
|
9927
9948
|
|
|
9928
9949
|
// src/teams/client.ts
|
|
9929
|
-
import { randomUUID } from "crypto";
|
|
9950
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
9930
9951
|
|
|
9931
9952
|
// src/framework/cloud-connect.ts
|
|
9932
9953
|
async function probeCloud(targetUrl) {
|
|
@@ -10051,6 +10072,7 @@ async function registerDirectViaPostgres(cloudUrl, email, name, teamName) {
|
|
|
10051
10072
|
}
|
|
10052
10073
|
|
|
10053
10074
|
// src/teams/direct-ops.ts
|
|
10075
|
+
import { randomUUID } from "crypto";
|
|
10054
10076
|
async function listMembersDirect(db, teamId) {
|
|
10055
10077
|
const members = await db.query("__lattice_team_members", {
|
|
10056
10078
|
filters: [{ col: "team_id", op: "eq", val: teamId }]
|
|
@@ -10117,6 +10139,293 @@ async function destroyTeamDirect(db) {
|
|
|
10117
10139
|
}
|
|
10118
10140
|
await db.delete("__lattice_team_identity", "singleton");
|
|
10119
10141
|
}
|
|
10142
|
+
async function redeemInviteDirect(cloudUrl, inviteToken, email, name) {
|
|
10143
|
+
if (!isPostgresUrl(cloudUrl)) {
|
|
10144
|
+
throw new Error(
|
|
10145
|
+
`redeemInviteDirect: cloudUrl must be a postgres:// URL (got ${cloudUrl.slice(0, 12)}\u2026)`
|
|
10146
|
+
);
|
|
10147
|
+
}
|
|
10148
|
+
const db = new Lattice(cloudUrl);
|
|
10149
|
+
try {
|
|
10150
|
+
await db.init();
|
|
10151
|
+
for (const [table, def] of Object.entries(CLOUD_INTERNAL_TABLE_DEFS)) {
|
|
10152
|
+
await db.defineLate(table, def);
|
|
10153
|
+
}
|
|
10154
|
+
const invites = await db.query("__lattice_invitations", {
|
|
10155
|
+
filters: [
|
|
10156
|
+
{ col: "token_hash", op: "eq", val: hashToken(inviteToken) },
|
|
10157
|
+
{ col: "redeemed_at", op: "isNull" }
|
|
10158
|
+
],
|
|
10159
|
+
limit: 1
|
|
10160
|
+
});
|
|
10161
|
+
const invite = invites[0];
|
|
10162
|
+
if (!invite) {
|
|
10163
|
+
throw new Error("Invitation invalid or already used");
|
|
10164
|
+
}
|
|
10165
|
+
if (invite.expires_at && new Date(invite.expires_at).getTime() < Date.now()) {
|
|
10166
|
+
throw new Error("Invitation expired");
|
|
10167
|
+
}
|
|
10168
|
+
if (invite.invitee_email && invite.invitee_email.toLowerCase() !== email.toLowerCase()) {
|
|
10169
|
+
throw new Error("Invitation is addressed to a different email");
|
|
10170
|
+
}
|
|
10171
|
+
const team = await db.get("__lattice_team", invite.team_id);
|
|
10172
|
+
if (!team || team.deleted_at) {
|
|
10173
|
+
throw new Error("Team no longer exists");
|
|
10174
|
+
}
|
|
10175
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10176
|
+
const userId = await db.insert("__lattice_users", {
|
|
10177
|
+
email,
|
|
10178
|
+
name,
|
|
10179
|
+
created_at: now,
|
|
10180
|
+
updated_at: now
|
|
10181
|
+
});
|
|
10182
|
+
await db.insert("__lattice_team_members", {
|
|
10183
|
+
team_id: invite.team_id,
|
|
10184
|
+
user_id: userId,
|
|
10185
|
+
role: "member",
|
|
10186
|
+
joined_at: now
|
|
10187
|
+
});
|
|
10188
|
+
const { raw, hash } = generateToken();
|
|
10189
|
+
await db.insert("__lattice_api_tokens", {
|
|
10190
|
+
user_id: userId,
|
|
10191
|
+
token_hash: hash,
|
|
10192
|
+
name: `invited:${team.name}`,
|
|
10193
|
+
created_at: now
|
|
10194
|
+
});
|
|
10195
|
+
await db.update("__lattice_invitations", invite.id, {
|
|
10196
|
+
redeemed_at: now,
|
|
10197
|
+
redeemed_by_user_id: userId
|
|
10198
|
+
});
|
|
10199
|
+
return {
|
|
10200
|
+
user: { id: userId, email, name },
|
|
10201
|
+
raw_token: raw,
|
|
10202
|
+
team: { id: team.id, name: team.name }
|
|
10203
|
+
};
|
|
10204
|
+
} finally {
|
|
10205
|
+
try {
|
|
10206
|
+
db.close();
|
|
10207
|
+
} catch {
|
|
10208
|
+
}
|
|
10209
|
+
}
|
|
10210
|
+
}
|
|
10211
|
+
async function openCloud(cloudUrl) {
|
|
10212
|
+
if (!isPostgresUrl(cloudUrl)) {
|
|
10213
|
+
throw new Error(
|
|
10214
|
+
`direct-ops: cloudUrl must be a postgres:// URL (got ${cloudUrl.slice(0, 12)}\u2026)`
|
|
10215
|
+
);
|
|
10216
|
+
}
|
|
10217
|
+
const db = new Lattice(cloudUrl);
|
|
10218
|
+
await db.init();
|
|
10219
|
+
for (const [table, def] of Object.entries(CLOUD_INTERNAL_TABLE_DEFS)) {
|
|
10220
|
+
await db.defineLate(table, def);
|
|
10221
|
+
}
|
|
10222
|
+
return db;
|
|
10223
|
+
}
|
|
10224
|
+
function closeQuiet(db) {
|
|
10225
|
+
try {
|
|
10226
|
+
db.close();
|
|
10227
|
+
} catch {
|
|
10228
|
+
}
|
|
10229
|
+
}
|
|
10230
|
+
async function appendChangeEnvelopeDirect(db, args) {
|
|
10231
|
+
const existing = await db.query("__lattice_change_log", {
|
|
10232
|
+
filters: [{ col: "team_id", op: "eq", val: args.team_id }],
|
|
10233
|
+
orderBy: "seq",
|
|
10234
|
+
orderDir: "desc",
|
|
10235
|
+
limit: 1
|
|
10236
|
+
});
|
|
10237
|
+
const nextSeq = (existing[0]?.seq ?? 0) + 1;
|
|
10238
|
+
await db.insert("__lattice_change_log", {
|
|
10239
|
+
id: randomUUID(),
|
|
10240
|
+
seq: nextSeq,
|
|
10241
|
+
team_id: args.team_id,
|
|
10242
|
+
table_name: args.table_name,
|
|
10243
|
+
pk: args.pk,
|
|
10244
|
+
op: args.op,
|
|
10245
|
+
payload_json: args.payload_json,
|
|
10246
|
+
owner_user_id: args.owner_user_id,
|
|
10247
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10248
|
+
});
|
|
10249
|
+
return nextSeq;
|
|
10250
|
+
}
|
|
10251
|
+
async function shareObjectDirect(cloudUrl, teamId, inviterUserId, table, spec) {
|
|
10252
|
+
const db = await openCloud(cloudUrl);
|
|
10253
|
+
try {
|
|
10254
|
+
const existing = await db.query("__lattice_shared_objects", {
|
|
10255
|
+
filters: [
|
|
10256
|
+
{ col: "team_id", op: "eq", val: teamId },
|
|
10257
|
+
{ col: "table_name", op: "eq", val: table }
|
|
10258
|
+
],
|
|
10259
|
+
limit: 1
|
|
10260
|
+
});
|
|
10261
|
+
const prior = existing[0];
|
|
10262
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10263
|
+
let schemaVersion;
|
|
10264
|
+
let outSpec;
|
|
10265
|
+
if (prior && !prior.deleted_at) {
|
|
10266
|
+
schemaVersion = prior.schema_version + 1;
|
|
10267
|
+
outSpec = { ...spec, schemaVersion };
|
|
10268
|
+
await db.upsert("__lattice_shared_objects", {
|
|
10269
|
+
team_id: teamId,
|
|
10270
|
+
table_name: table,
|
|
10271
|
+
schema_spec_json: JSON.stringify(outSpec),
|
|
10272
|
+
schema_version: schemaVersion,
|
|
10273
|
+
created_by_user_id: prior.created_by_user_id,
|
|
10274
|
+
created_at: prior.created_at,
|
|
10275
|
+
updated_at: now,
|
|
10276
|
+
deleted_at: null
|
|
10277
|
+
});
|
|
10278
|
+
} else {
|
|
10279
|
+
schemaVersion = 1;
|
|
10280
|
+
outSpec = { ...spec, schemaVersion };
|
|
10281
|
+
await db.upsert("__lattice_shared_objects", {
|
|
10282
|
+
team_id: teamId,
|
|
10283
|
+
table_name: table,
|
|
10284
|
+
schema_spec_json: JSON.stringify(outSpec),
|
|
10285
|
+
schema_version: schemaVersion,
|
|
10286
|
+
created_by_user_id: inviterUserId,
|
|
10287
|
+
created_at: prior?.created_at ?? now,
|
|
10288
|
+
updated_at: now,
|
|
10289
|
+
deleted_at: null
|
|
10290
|
+
});
|
|
10291
|
+
}
|
|
10292
|
+
await applySchemaSpec(db, table, outSpec);
|
|
10293
|
+
const seq = await appendChangeEnvelopeDirect(db, {
|
|
10294
|
+
team_id: teamId,
|
|
10295
|
+
table_name: table,
|
|
10296
|
+
pk: null,
|
|
10297
|
+
op: "schema",
|
|
10298
|
+
payload_json: JSON.stringify(outSpec),
|
|
10299
|
+
owner_user_id: null
|
|
10300
|
+
});
|
|
10301
|
+
return { table, schema_version: schemaVersion, seq, schema_spec: outSpec };
|
|
10302
|
+
} finally {
|
|
10303
|
+
closeQuiet(db);
|
|
10304
|
+
}
|
|
10305
|
+
}
|
|
10306
|
+
async function listSharedObjectsDirect(cloudUrl, teamId) {
|
|
10307
|
+
const db = await openCloud(cloudUrl);
|
|
10308
|
+
try {
|
|
10309
|
+
const rows = await db.query("__lattice_shared_objects", {
|
|
10310
|
+
filters: [
|
|
10311
|
+
{ col: "team_id", op: "eq", val: teamId },
|
|
10312
|
+
{ col: "deleted_at", op: "isNull" }
|
|
10313
|
+
]
|
|
10314
|
+
});
|
|
10315
|
+
return rows.map((r) => ({
|
|
10316
|
+
table: r.table_name,
|
|
10317
|
+
schema_version: r.schema_version,
|
|
10318
|
+
created_by_user_id: r.created_by_user_id,
|
|
10319
|
+
created_at: r.created_at,
|
|
10320
|
+
updated_at: r.updated_at,
|
|
10321
|
+
schema_spec: JSON.parse(r.schema_spec_json)
|
|
10322
|
+
}));
|
|
10323
|
+
} finally {
|
|
10324
|
+
closeQuiet(db);
|
|
10325
|
+
}
|
|
10326
|
+
}
|
|
10327
|
+
async function unshareObjectDirect(cloudUrl, teamId, table) {
|
|
10328
|
+
const db = await openCloud(cloudUrl);
|
|
10329
|
+
try {
|
|
10330
|
+
const existing = await db.query("__lattice_shared_objects", {
|
|
10331
|
+
filters: [
|
|
10332
|
+
{ col: "team_id", op: "eq", val: teamId },
|
|
10333
|
+
{ col: "table_name", op: "eq", val: table },
|
|
10334
|
+
{ col: "deleted_at", op: "isNull" }
|
|
10335
|
+
],
|
|
10336
|
+
limit: 1
|
|
10337
|
+
});
|
|
10338
|
+
const row = existing[0];
|
|
10339
|
+
if (!row) return;
|
|
10340
|
+
await db.upsert("__lattice_shared_objects", {
|
|
10341
|
+
team_id: teamId,
|
|
10342
|
+
table_name: table,
|
|
10343
|
+
schema_spec_json: row.schema_spec_json,
|
|
10344
|
+
schema_version: row.schema_version,
|
|
10345
|
+
created_by_user_id: row.created_by_user_id,
|
|
10346
|
+
created_at: row.created_at,
|
|
10347
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10348
|
+
deleted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10349
|
+
});
|
|
10350
|
+
await appendChangeEnvelopeDirect(db, {
|
|
10351
|
+
team_id: teamId,
|
|
10352
|
+
table_name: table,
|
|
10353
|
+
pk: null,
|
|
10354
|
+
op: "unshare",
|
|
10355
|
+
payload_json: null,
|
|
10356
|
+
owner_user_id: null
|
|
10357
|
+
});
|
|
10358
|
+
} finally {
|
|
10359
|
+
closeQuiet(db);
|
|
10360
|
+
}
|
|
10361
|
+
}
|
|
10362
|
+
async function meDirect(cloudUrl, bearerToken) {
|
|
10363
|
+
const db = await openCloud(cloudUrl);
|
|
10364
|
+
try {
|
|
10365
|
+
const tokens = await db.query("__lattice_api_tokens", {
|
|
10366
|
+
filters: [{ col: "token_hash", op: "eq", val: hashToken(bearerToken) }],
|
|
10367
|
+
limit: 1
|
|
10368
|
+
});
|
|
10369
|
+
const tok = tokens[0];
|
|
10370
|
+
if (!tok || tok.revoked_at) throw new Error("Unauthorized");
|
|
10371
|
+
const user = await db.get("__lattice_users", tok.user_id);
|
|
10372
|
+
if (!user || user.deleted_at) throw new Error("Unauthorized");
|
|
10373
|
+
return { user: { id: user.id, email: user.email, name: user.name } };
|
|
10374
|
+
} finally {
|
|
10375
|
+
closeQuiet(db);
|
|
10376
|
+
}
|
|
10377
|
+
}
|
|
10378
|
+
async function linkRowDirect(local, cloudUrl, teamId, myUserId, table, pk) {
|
|
10379
|
+
const cloud = await openCloud(cloudUrl);
|
|
10380
|
+
let seq;
|
|
10381
|
+
try {
|
|
10382
|
+
await cloud.upsert("__lattice_row_links", {
|
|
10383
|
+
team_id: teamId,
|
|
10384
|
+
table_name: table,
|
|
10385
|
+
pk,
|
|
10386
|
+
owner_user_id: myUserId,
|
|
10387
|
+
linked_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10388
|
+
});
|
|
10389
|
+
seq = await appendChangeEnvelopeDirect(cloud, {
|
|
10390
|
+
team_id: teamId,
|
|
10391
|
+
table_name: table,
|
|
10392
|
+
pk,
|
|
10393
|
+
op: "link",
|
|
10394
|
+
payload_json: JSON.stringify({ owner_user_id: myUserId }),
|
|
10395
|
+
owner_user_id: myUserId
|
|
10396
|
+
});
|
|
10397
|
+
} finally {
|
|
10398
|
+
closeQuiet(cloud);
|
|
10399
|
+
}
|
|
10400
|
+
await local.upsert("__lattice_local_links", {
|
|
10401
|
+
team_id: teamId,
|
|
10402
|
+
table_name: table,
|
|
10403
|
+
pk,
|
|
10404
|
+
owner_user_id: myUserId,
|
|
10405
|
+
linked_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10406
|
+
});
|
|
10407
|
+
return { owner_user_id: myUserId, seq };
|
|
10408
|
+
}
|
|
10409
|
+
async function unlinkRowDirect(local, cloudUrl, teamId, table, pk) {
|
|
10410
|
+
const cloud = await openCloud(cloudUrl);
|
|
10411
|
+
try {
|
|
10412
|
+
await cloud.delete("__lattice_row_links", { team_id: teamId, table_name: table, pk });
|
|
10413
|
+
await appendChangeEnvelopeDirect(cloud, {
|
|
10414
|
+
team_id: teamId,
|
|
10415
|
+
table_name: table,
|
|
10416
|
+
pk,
|
|
10417
|
+
op: "unlink",
|
|
10418
|
+
payload_json: null,
|
|
10419
|
+
owner_user_id: null
|
|
10420
|
+
});
|
|
10421
|
+
} finally {
|
|
10422
|
+
closeQuiet(cloud);
|
|
10423
|
+
}
|
|
10424
|
+
try {
|
|
10425
|
+
await local.delete("__lattice_local_links", { team_id: teamId, table_name: table, pk });
|
|
10426
|
+
} catch {
|
|
10427
|
+
}
|
|
10428
|
+
}
|
|
10120
10429
|
|
|
10121
10430
|
// src/teams/client.ts
|
|
10122
10431
|
var TeamsClient = class {
|
|
@@ -10160,6 +10469,9 @@ var TeamsClient = class {
|
|
|
10160
10469
|
});
|
|
10161
10470
|
}
|
|
10162
10471
|
async redeemInvite(cloudUrl, inviteToken, email, name) {
|
|
10472
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
10473
|
+
return redeemInviteDirect(cloudUrl, inviteToken, email, name);
|
|
10474
|
+
}
|
|
10163
10475
|
return this.fetchUnauthed(cloudUrl, "POST", "/api/auth/redeem-invite", {
|
|
10164
10476
|
invite_token: inviteToken,
|
|
10165
10477
|
email,
|
|
@@ -10315,6 +10627,9 @@ var TeamsClient = class {
|
|
|
10315
10627
|
);
|
|
10316
10628
|
}
|
|
10317
10629
|
async me(cloudUrl, token) {
|
|
10630
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
10631
|
+
return meDirect(cloudUrl, token);
|
|
10632
|
+
}
|
|
10318
10633
|
return this.fetchAuthed(cloudUrl, token, "GET", "/api/auth/me");
|
|
10319
10634
|
}
|
|
10320
10635
|
// ── Object sharing (Phase 3) ────────────────────────────────────────────
|
|
@@ -10323,7 +10638,15 @@ var TeamsClient = class {
|
|
|
10323
10638
|
* `schema_version` and replaces the stored spec — useful for evolving
|
|
10324
10639
|
* shared schemas additively.
|
|
10325
10640
|
*/
|
|
10326
|
-
async shareObject(cloudUrl, token, teamId, table, schemaSpec) {
|
|
10641
|
+
async shareObject(cloudUrl, token, teamId, table, schemaSpec, inviterUserId) {
|
|
10642
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
10643
|
+
if (!inviterUserId) {
|
|
10644
|
+
throw new Error(
|
|
10645
|
+
"shareObject: inviterUserId is required for direct-Postgres cloud URLs (read it from __lattice_team_connections.my_user_id)"
|
|
10646
|
+
);
|
|
10647
|
+
}
|
|
10648
|
+
return shareObjectDirect(cloudUrl, teamId, inviterUserId, table, schemaSpec);
|
|
10649
|
+
}
|
|
10327
10650
|
return this.fetchAuthed(
|
|
10328
10651
|
cloudUrl,
|
|
10329
10652
|
token,
|
|
@@ -10338,6 +10661,10 @@ var TeamsClient = class {
|
|
|
10338
10661
|
* row and appends an `unshare` envelope to the change log.
|
|
10339
10662
|
*/
|
|
10340
10663
|
async unshareObject(cloudUrl, token, teamId, table) {
|
|
10664
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
10665
|
+
await unshareObjectDirect(cloudUrl, teamId, table);
|
|
10666
|
+
return;
|
|
10667
|
+
}
|
|
10341
10668
|
await this.fetchAuthed(
|
|
10342
10669
|
cloudUrl,
|
|
10343
10670
|
token,
|
|
@@ -10346,6 +10673,9 @@ var TeamsClient = class {
|
|
|
10346
10673
|
);
|
|
10347
10674
|
}
|
|
10348
10675
|
async listSharedObjects(cloudUrl, token, teamId) {
|
|
10676
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
10677
|
+
return listSharedObjectsDirect(cloudUrl, teamId);
|
|
10678
|
+
}
|
|
10349
10679
|
const r = await this.fetchAuthed(
|
|
10350
10680
|
cloudUrl,
|
|
10351
10681
|
token,
|
|
@@ -10485,6 +10815,18 @@ var TeamsClient = class {
|
|
|
10485
10815
|
if (!snapshot) {
|
|
10486
10816
|
throw new Error(`Row not found in local table "${table}" with pk "${pk}"`);
|
|
10487
10817
|
}
|
|
10818
|
+
if (isPostgresUrl(connection.cloud_url)) {
|
|
10819
|
+
const result2 = await linkRowDirect(
|
|
10820
|
+
this.local,
|
|
10821
|
+
connection.cloud_url,
|
|
10822
|
+
connection.team_id,
|
|
10823
|
+
connection.my_user_id,
|
|
10824
|
+
table,
|
|
10825
|
+
pk
|
|
10826
|
+
);
|
|
10827
|
+
this.ensureWriteHook(table);
|
|
10828
|
+
return result2;
|
|
10829
|
+
}
|
|
10488
10830
|
const result = await this.fetchAuthed(
|
|
10489
10831
|
connection.cloud_url,
|
|
10490
10832
|
connection.api_token,
|
|
@@ -10510,6 +10852,10 @@ var TeamsClient = class {
|
|
|
10510
10852
|
*/
|
|
10511
10853
|
async unlinkRow(connection, table, pk) {
|
|
10512
10854
|
await this.ensureLocalTables();
|
|
10855
|
+
if (isPostgresUrl(connection.cloud_url)) {
|
|
10856
|
+
await unlinkRowDirect(this.local, connection.cloud_url, connection.team_id, table, pk);
|
|
10857
|
+
return;
|
|
10858
|
+
}
|
|
10513
10859
|
await this.fetchAuthed(
|
|
10514
10860
|
connection.cloud_url,
|
|
10515
10861
|
connection.api_token,
|
|
@@ -10585,7 +10931,7 @@ var TeamsClient = class {
|
|
|
10585
10931
|
if (conn.my_user_id !== link.owner_user_id) continue;
|
|
10586
10932
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10587
10933
|
await this.local.insert("__lattice_team_outbox", {
|
|
10588
|
-
id:
|
|
10934
|
+
id: randomUUID2(),
|
|
10589
10935
|
team_id: link.team_id,
|
|
10590
10936
|
table_name: ctx.table,
|
|
10591
10937
|
pk: ctx.pk,
|
|
@@ -10611,6 +10957,9 @@ var TeamsClient = class {
|
|
|
10611
10957
|
*/
|
|
10612
10958
|
async drainOutbox(connection) {
|
|
10613
10959
|
await this.ensureLocalTables();
|
|
10960
|
+
if (isPostgresUrl(connection.cloud_url)) {
|
|
10961
|
+
return { pushed: 0, failed: 0 };
|
|
10962
|
+
}
|
|
10614
10963
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10615
10964
|
const rows = await this.local.query("__lattice_team_outbox", {
|
|
10616
10965
|
filters: [
|
|
@@ -10671,6 +11020,9 @@ var TeamsClient = class {
|
|
|
10671
11020
|
*/
|
|
10672
11021
|
async pullChanges(connection, batchSize = 500) {
|
|
10673
11022
|
await this.ensureLocalTables();
|
|
11023
|
+
if (isPostgresUrl(connection.cloud_url)) {
|
|
11024
|
+
return { applied: 0, last_seq: 0, dlq_count: 0 };
|
|
11025
|
+
}
|
|
10674
11026
|
const connRow = await this.local.get(
|
|
10675
11027
|
"__lattice_team_connections",
|
|
10676
11028
|
connection.team_id
|
|
@@ -10694,7 +11046,7 @@ var TeamsClient = class {
|
|
|
10694
11046
|
totalApplied++;
|
|
10695
11047
|
} catch (e) {
|
|
10696
11048
|
await this.local.insert("__lattice_team_dlq", {
|
|
10697
|
-
id:
|
|
11049
|
+
id: randomUUID2(),
|
|
10698
11050
|
team_id: connection.team_id,
|
|
10699
11051
|
envelope_json: JSON.stringify(env),
|
|
10700
11052
|
error: e.message,
|
|
@@ -10836,6 +11188,23 @@ var TeamsHttpError = class extends Error {
|
|
|
10836
11188
|
};
|
|
10837
11189
|
|
|
10838
11190
|
// src/gui/teams-routes.ts
|
|
11191
|
+
import { existsSync as existsSync12, writeFileSync as writeFileSync5 } from "fs";
|
|
11192
|
+
import { dirname as dirname5, join as join12 } from "path";
|
|
11193
|
+
function writeTeamConfigYaml(activeConfigPath, credentialLabel, teamName) {
|
|
11194
|
+
const projectDir = dirname5(activeConfigPath);
|
|
11195
|
+
const yamlPath = join12(projectDir, `${credentialLabel}.yml`);
|
|
11196
|
+
if (existsSync12(yamlPath)) return yamlPath;
|
|
11197
|
+
const yaml = `# Joined-team config \u2014 managed by lattice gui. Edit entities: to add
|
|
11198
|
+
# locally-projected tables of the team's shared data; the cloud DB at
|
|
11199
|
+
# ${credentialLabel} is the authoritative source.
|
|
11200
|
+
db: \${LATTICE_DB:${credentialLabel}}
|
|
11201
|
+
|
|
11202
|
+
# Team: ${teamName.replace(/[\r\n]/g, " ")}
|
|
11203
|
+
entities: {}
|
|
11204
|
+
`;
|
|
11205
|
+
writeFileSync5(yamlPath, yaml, "utf8");
|
|
11206
|
+
return yamlPath;
|
|
11207
|
+
}
|
|
10839
11208
|
function sendJson2(res, body, status = 200) {
|
|
10840
11209
|
res.writeHead(status, {
|
|
10841
11210
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -11008,7 +11377,8 @@ async function dispatchTeamSubroute(req, res, ctx, teamId, subpath) {
|
|
|
11008
11377
|
conn.api_token,
|
|
11009
11378
|
teamId,
|
|
11010
11379
|
table,
|
|
11011
|
-
spec
|
|
11380
|
+
spec,
|
|
11381
|
+
conn.my_user_id
|
|
11012
11382
|
);
|
|
11013
11383
|
sendJson2(res, result);
|
|
11014
11384
|
return;
|
|
@@ -11060,7 +11430,19 @@ async function handleJoin(req, res, ctx) {
|
|
|
11060
11430
|
my_user_id: result.user.id,
|
|
11061
11431
|
api_token: result.raw_token
|
|
11062
11432
|
});
|
|
11063
|
-
|
|
11433
|
+
const credentialLabel = saveDbCredentialForTeam({
|
|
11434
|
+
teamName: result.team.name,
|
|
11435
|
+
teamId: result.team.id,
|
|
11436
|
+
cloudUrl
|
|
11437
|
+
});
|
|
11438
|
+
const configYamlPath = writeTeamConfigYaml(ctx.configPath, credentialLabel, result.team.name);
|
|
11439
|
+
sendJson2(res, {
|
|
11440
|
+
ok: true,
|
|
11441
|
+
team: result.team,
|
|
11442
|
+
user: result.user,
|
|
11443
|
+
credential_label: credentialLabel,
|
|
11444
|
+
config_path: configYamlPath
|
|
11445
|
+
});
|
|
11064
11446
|
}
|
|
11065
11447
|
async function handleRegisterAndCreate(req, res, ctx) {
|
|
11066
11448
|
const body = await readJson2(req);
|
|
@@ -11108,8 +11490,8 @@ async function handleLeave(res, ctx, teamId) {
|
|
|
11108
11490
|
}
|
|
11109
11491
|
|
|
11110
11492
|
// src/gui/userconfig-routes.ts
|
|
11111
|
-
import { existsSync as
|
|
11112
|
-
import { basename as basename4, dirname as
|
|
11493
|
+
import { existsSync as existsSync13, readdirSync as readdirSync5 } from "fs";
|
|
11494
|
+
import { basename as basename4, dirname as dirname6, join as join13 } from "path";
|
|
11113
11495
|
function sendJson3(res, body, status = 200) {
|
|
11114
11496
|
res.writeHead(status, {
|
|
11115
11497
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -11161,12 +11543,12 @@ async function upsertIdentityRow(db, identity) {
|
|
|
11161
11543
|
}
|
|
11162
11544
|
}
|
|
11163
11545
|
function listProjectConfigs(activeConfigPath) {
|
|
11164
|
-
const dir =
|
|
11546
|
+
const dir = dirname6(activeConfigPath);
|
|
11165
11547
|
const out = [];
|
|
11166
|
-
if (!
|
|
11548
|
+
if (!existsSync13(dir)) return out;
|
|
11167
11549
|
for (const fname of readdirSync5(dir)) {
|
|
11168
11550
|
if (!fname.endsWith(".yml") && !fname.endsWith(".yaml")) continue;
|
|
11169
|
-
const full =
|
|
11551
|
+
const full = join13(dir, fname);
|
|
11170
11552
|
try {
|
|
11171
11553
|
const parsed = parseConfigFile(full);
|
|
11172
11554
|
out.push({
|
|
@@ -11231,12 +11613,12 @@ async function dispatchUserConfigRoute(req, res, ctx) {
|
|
|
11231
11613
|
}
|
|
11232
11614
|
|
|
11233
11615
|
// src/gui/dbconfig-routes.ts
|
|
11234
|
-
import { readFileSync as readFileSync10, writeFileSync as
|
|
11616
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
|
|
11235
11617
|
import { basename as basename5, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve5 } from "path";
|
|
11236
11618
|
import { parseDocument } from "yaml";
|
|
11237
11619
|
|
|
11238
11620
|
// src/framework/cloud-migration.ts
|
|
11239
|
-
import { existsSync as
|
|
11621
|
+
import { existsSync as existsSync14, renameSync as renameSync2, unlinkSync as unlinkSync4 } from "fs";
|
|
11240
11622
|
|
|
11241
11623
|
// src/framework/native-entities.ts
|
|
11242
11624
|
var NATIVE_ENTITY_DEFS = {
|
|
@@ -11343,14 +11725,14 @@ async function openTargetLatticeForMigration(configPath, targetUrl, encryptionKe
|
|
|
11343
11725
|
return target;
|
|
11344
11726
|
}
|
|
11345
11727
|
function archiveLocalSqlite(dbPath) {
|
|
11346
|
-
if (!
|
|
11728
|
+
if (!existsSync14(dbPath)) {
|
|
11347
11729
|
throw new Error(`archiveLocalSqlite: source file does not exist: ${dbPath}`);
|
|
11348
11730
|
}
|
|
11349
11731
|
const backupPath = `${dbPath}.local-bak`;
|
|
11350
11732
|
const siblings = ["", "-shm", "-wal"];
|
|
11351
11733
|
for (const suffix of siblings) {
|
|
11352
11734
|
const stale = `${dbPath}.local-bak${suffix}`;
|
|
11353
|
-
if (
|
|
11735
|
+
if (existsSync14(stale)) {
|
|
11354
11736
|
try {
|
|
11355
11737
|
unlinkSync4(stale);
|
|
11356
11738
|
} catch {
|
|
@@ -11359,7 +11741,7 @@ function archiveLocalSqlite(dbPath) {
|
|
|
11359
11741
|
}
|
|
11360
11742
|
for (const suffix of siblings) {
|
|
11361
11743
|
const src = `${dbPath}${suffix}`;
|
|
11362
|
-
if (!
|
|
11744
|
+
if (!existsSync14(src)) continue;
|
|
11363
11745
|
const dest = `${dbPath}.local-bak${suffix}`;
|
|
11364
11746
|
renameSync2(src, dest);
|
|
11365
11747
|
}
|
|
@@ -11528,7 +11910,7 @@ async function detectTeamEnabled(db) {
|
|
|
11528
11910
|
function rewriteDbLine(configPath, newValue) {
|
|
11529
11911
|
const doc = parseDocument(readFileSync10(configPath, "utf8"));
|
|
11530
11912
|
doc.set("db", newValue);
|
|
11531
|
-
|
|
11913
|
+
writeFileSync6(configPath, doc.toString(), "utf8");
|
|
11532
11914
|
}
|
|
11533
11915
|
function parseSaveBody(body) {
|
|
11534
11916
|
const type = body.type;
|
|
@@ -11999,9 +12381,9 @@ function readRowContext(outputDir, locator, secretCols) {
|
|
|
11999
12381
|
throw new Error(`Path traversal detected: slug "${slug}" escapes output directory`);
|
|
12000
12382
|
}
|
|
12001
12383
|
return fileNames.map((filename) => {
|
|
12002
|
-
const absPath =
|
|
12003
|
-
const relPath =
|
|
12004
|
-
if (!
|
|
12384
|
+
const absPath = join14(entityDir, filename);
|
|
12385
|
+
const relPath = join14(directoryRoot, slug, filename);
|
|
12386
|
+
if (!existsSync15(absPath)) return { name: filename, path: relPath, content: "" };
|
|
12005
12387
|
let content = readFileSync11(absPath, "utf8");
|
|
12006
12388
|
for (const col of secretCols) {
|
|
12007
12389
|
const re = new RegExp(`^(${col}):.*$`, "gm");
|
|
@@ -12012,7 +12394,7 @@ function readRowContext(outputDir, locator, secretCols) {
|
|
|
12012
12394
|
}
|
|
12013
12395
|
async function openConfig(configPath, outputDir) {
|
|
12014
12396
|
const parsed = parseConfigFile(configPath);
|
|
12015
|
-
mkdirSync6(
|
|
12397
|
+
mkdirSync6(dirname7(parsed.dbPath), { recursive: true });
|
|
12016
12398
|
const encryptionKey = getOrCreateMasterKey();
|
|
12017
12399
|
const db = new Lattice({ config: configPath }, { encryptionKey });
|
|
12018
12400
|
registerNativeEntities(db);
|
|
@@ -12096,11 +12478,11 @@ async function openConfig(configPath, outputDir) {
|
|
|
12096
12478
|
};
|
|
12097
12479
|
}
|
|
12098
12480
|
function listConfigs(activeConfigPath) {
|
|
12099
|
-
const dir =
|
|
12481
|
+
const dir = dirname7(activeConfigPath);
|
|
12100
12482
|
const entries = [];
|
|
12101
12483
|
for (const fname of readdirSync6(dir)) {
|
|
12102
12484
|
if (!fname.endsWith(".yml") && !fname.endsWith(".yaml")) continue;
|
|
12103
|
-
const full =
|
|
12485
|
+
const full = join14(dir, fname);
|
|
12104
12486
|
try {
|
|
12105
12487
|
const parsed = parseConfigFile(full);
|
|
12106
12488
|
entries.push({
|
|
@@ -12123,14 +12505,14 @@ function loadConfigDoc(configPath) {
|
|
|
12123
12505
|
return parseDocument2(readFileSync11(configPath, "utf8"));
|
|
12124
12506
|
}
|
|
12125
12507
|
function saveConfigDoc(configPath, doc) {
|
|
12126
|
-
|
|
12508
|
+
writeFileSync7(configPath, doc.toString(), "utf8");
|
|
12127
12509
|
}
|
|
12128
12510
|
function createBlankConfig(activeConfigPath, dbName) {
|
|
12129
|
-
const dir =
|
|
12511
|
+
const dir = dirname7(activeConfigPath);
|
|
12130
12512
|
const slug = dbName.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
12131
12513
|
if (!slug) throw new Error("Database name must contain at least one alphanumeric character");
|
|
12132
|
-
const configPath =
|
|
12133
|
-
if (
|
|
12514
|
+
const configPath = join14(dir, `${slug}.config.yml`);
|
|
12515
|
+
if (existsSync15(configPath)) throw new Error(`Config already exists: ${slug}.config.yml`);
|
|
12134
12516
|
const yaml = `db: ./data/${slug}.db
|
|
12135
12517
|
|
|
12136
12518
|
entities:
|
|
@@ -12142,8 +12524,8 @@ entities:
|
|
|
12142
12524
|
deleted_at: { type: text }
|
|
12143
12525
|
outputFile: ITEMS.md
|
|
12144
12526
|
`;
|
|
12145
|
-
|
|
12146
|
-
mkdirSync6(
|
|
12527
|
+
writeFileSync7(configPath, yaml, "utf8");
|
|
12528
|
+
mkdirSync6(join14(dir, "data"), { recursive: true });
|
|
12147
12529
|
return configPath;
|
|
12148
12530
|
}
|
|
12149
12531
|
async function registerTeamCloudTables(db) {
|
|
@@ -12521,7 +12903,7 @@ async function startGuiServer(options) {
|
|
|
12521
12903
|
return;
|
|
12522
12904
|
}
|
|
12523
12905
|
const newPath = resolve6(body.path);
|
|
12524
|
-
if (!
|
|
12906
|
+
if (!existsSync15(newPath)) {
|
|
12525
12907
|
sendJson5(res, { error: `Config not found: ${newPath}` }, 400);
|
|
12526
12908
|
return;
|
|
12527
12909
|
}
|
|
@@ -12723,6 +13105,7 @@ async function startGuiServer(options) {
|
|
|
12723
13105
|
const handled = await dispatchTeamsGuiRoute(req, res, {
|
|
12724
13106
|
db: active.db,
|
|
12725
13107
|
client: active.teamsClient,
|
|
13108
|
+
configPath: active.configPath,
|
|
12726
13109
|
pathname,
|
|
12727
13110
|
method,
|
|
12728
13111
|
validTables: active.validTables
|
|
@@ -12780,13 +13163,13 @@ async function startGuiServer(options) {
|
|
|
12780
13163
|
}
|
|
12781
13164
|
|
|
12782
13165
|
// src/gui/discover-output-dir.ts
|
|
12783
|
-
import { existsSync as
|
|
12784
|
-
import { join as
|
|
13166
|
+
import { existsSync as existsSync16 } from "fs";
|
|
13167
|
+
import { join as join15, resolve as resolve7 } from "path";
|
|
12785
13168
|
function discoverOutputDir(explicitOutput, explicit) {
|
|
12786
13169
|
if (explicit) return explicitOutput;
|
|
12787
13170
|
const candidates = ["./context", ".", "./generated"];
|
|
12788
13171
|
for (const dir of candidates) {
|
|
12789
|
-
if (
|
|
13172
|
+
if (existsSync16(join15(resolve7(dir), ".lattice", "manifest.json"))) return dir;
|
|
12790
13173
|
}
|
|
12791
13174
|
return explicitOutput;
|
|
12792
13175
|
}
|
|
@@ -13504,7 +13887,7 @@ function runGenerate(args) {
|
|
|
13504
13887
|
console.error('Error: config must have an "entities" key');
|
|
13505
13888
|
process.exit(1);
|
|
13506
13889
|
}
|
|
13507
|
-
const configDir2 =
|
|
13890
|
+
const configDir2 = dirname8(configPath);
|
|
13508
13891
|
const outDir = resolve9(args.out);
|
|
13509
13892
|
try {
|
|
13510
13893
|
const result = generateAll({ config, configDir: configDir2, outDir, scaffold: args.scaffold });
|