latticesql 2.1.1 → 2.2.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/dist/index.d.cts CHANGED
@@ -1969,6 +1969,76 @@ declare class Lattice {
1969
1969
  */
1970
1970
  search(table: string, query: string, opts?: SearchOptions): Promise<SearchResult[]>;
1971
1971
  query(table: string, opts?: QueryOptions): Promise<Row[]>;
1972
+ /**
1973
+ * Row-level-security list read for Lattice Teams (2.2). Returns only the
1974
+ * rows of `table` that `userId` may see in team `teamId`, evaluated
1975
+ * entirely in SQL (indexed, bounded — never "load every row then filter
1976
+ * in JS"). A row is visible iff it has a `__lattice_row_acl` entry owned by
1977
+ * the user or marked 'everyone', or a 'custom' entry with a matching
1978
+ * `__lattice_row_grants` row, OR it has no ACL entry at all and the caller
1979
+ * passes `noAclVisible` (the table default is 'everyone', or the user owns
1980
+ * the table — the pre-2.2 / never-narrowed case). Soft-deleted rows are
1981
+ * excluded by default; results reuse the same decrypt path as `query()`.
1982
+ *
1983
+ * The ACL predicate joins on the table's primary-key column cast to TEXT
1984
+ * (ACL pks are stored as TEXT), so it is correct regardless of the user
1985
+ * table's pk type and works on both SQLite and Postgres. The teams layer's
1986
+ * `listVisibleRows` (src/teams/row-access.ts) is the intended caller.
1987
+ */
1988
+ queryVisible(table: string, opts: {
1989
+ teamId: string;
1990
+ userId: string;
1991
+ /**
1992
+ * Whether rows with NO `__lattice_row_acl` entry are visible to this
1993
+ * user — true when the table default is 'everyone' OR the user owns the
1994
+ * table (the pre-2.2 / never-narrowed case). Resolved by the teams layer
1995
+ * (`listVisibleRows`); defaults to false, i.e. only rows with an explicit
1996
+ * ACL entry granting access are returned.
1997
+ */
1998
+ noAclVisible?: boolean;
1999
+ /** Soft-delete handling: 'exclude' (default), 'only' (trash), 'any'. */
2000
+ deleted?: 'exclude' | 'only' | 'any';
2001
+ limit?: number;
2002
+ offset?: number;
2003
+ orderBy?: string;
2004
+ orderDir?: 'asc' | 'desc';
2005
+ }): Promise<Row[]>;
2006
+ /**
2007
+ * Visible-row counts for MANY tables in a single round-trip, using the same
2008
+ * ACL predicate as {@link queryVisible} — so dashboard tiles agree with what
2009
+ * the rows view lists and a physical count never reveals the existence or
2010
+ * volume of rows the user can't see. One aggregated
2011
+ * `SELECT (SELECT COUNT(*) …) AS c0, …` statement (no per-table fan-out, so
2012
+ * a session pooler with few slots survives concurrent refreshes), capped at
2013
+ * 50 tables per pass; overflow is logged and skipped (no silent truncation)
2014
+ * and those tables count as absent — the caller renders "—". Soft-deleted
2015
+ * rows are excluded wherever the table carries `deleted_at`, matching the
2016
+ * default rows view.
2017
+ */
2018
+ countVisibleMany(specs: {
2019
+ table: string;
2020
+ noAclVisible: boolean;
2021
+ }[], opts: {
2022
+ teamId: string;
2023
+ userId: string;
2024
+ }): Promise<Map<string, number>>;
2025
+ /**
2026
+ * Hosted-sync change-log pull, filtered per recipient for 2.2 row-level
2027
+ * security (the hosted server's sole enforcement mechanism). Returns
2028
+ * `__lattice_change_log` rows with seq > `since` for team `teamId` that
2029
+ * `userId` is permitted to receive:
2030
+ * - targeted envelopes (`recipient_user_id = userId`), plus
2031
+ * - broadcast envelopes (`recipient_user_id IS NULL`) that are either
2032
+ * table-level (`pk IS NULL` — schema / unshare, delivered to all) or
2033
+ * whose row is currently visible to the user via `__lattice_row_acl` /
2034
+ * `__lattice_row_grants` (or has no ACL entry and the table defaults to
2035
+ * 'everyone').
2036
+ * Ordered by seq, capped at `limit`. Raw SQL because the predicate needs
2037
+ * OR / EXISTS that the `query()` API can't express; bounded by the seq
2038
+ * window and indexed ACL point-lookups. Mirrors {@link queryVisible}'s
2039
+ * visibility logic so a member never pulls the bytes of a row they can't see.
2040
+ */
2041
+ listChangesForRecipient(teamId: string, since: number, userId: string, limit: number): Promise<Row[]>;
1972
2042
  count(table: string, opts?: CountOptions): Promise<number>;
1973
2043
  render(outputDir: string): Promise<RenderResult>;
1974
2044
  sync(outputDir: string): Promise<SyncResult>;
@@ -2041,6 +2111,31 @@ declare class Lattice {
2041
2111
  * - `Record` → matches every PK column; all must be present in the object.
2042
2112
  */
2043
2113
  private _pkWhere;
2114
+ private static readonly _PK_SEP;
2115
+ /**
2116
+ * The primary-key columns of `table` that PHYSICALLY exist, in declared
2117
+ * order. Empty when the table has no Lattice-addressable key — e.g. a table
2118
+ * reached via raw SQL whose PK metadata defaulted to `['id']` but that has
2119
+ * no `id` column. Callers treat an empty result as "unkeyable" (no per-row
2120
+ * ACL is possible, so the row-perm SQL must not reference a pk column).
2121
+ */
2122
+ private _resolvedPkCols;
2123
+ /** Canonical ACL / change-log `pk` string for a row. Matches {@link _pkSqlExpr}. */
2124
+ private _serializeRowPk;
2125
+ /**
2126
+ * Canonical `pk` string for a {@link PkLookup} used by update/delete, so a
2127
+ * row addressed by lookup keys its change-log entry identically to the way
2128
+ * {@link _serializeRowPk} keyed it at insert time.
2129
+ */
2130
+ private _serializePkLookup;
2131
+ /**
2132
+ * SQL expression reconstructing {@link _serializeRowPk} from a row aliased
2133
+ * `t`. Returns null when the table is unkeyable (no pk columns present) — the
2134
+ * caller must then avoid referencing a pk column at all. Dialect-aware tab
2135
+ * separator: SQLite `char(9)`, Postgres `chr(9)` (both = U+0009), matching
2136
+ * {@link _PK_SEP}.
2137
+ */
2138
+ private _pkSqlExpr;
2044
2139
  /**
2045
2140
  * Convert Filter objects into SQL clause strings and bound params.
2046
2141
  * An `in` filter with an empty array is silently ignored (produces no clause).
@@ -3722,6 +3817,9 @@ declare class TeamsClient {
3722
3817
  * members join via `redeemInvite`). Returns the new user + bearer
3723
3818
  * token + team summary so the caller can immediately save a
3724
3819
  * connection.
3820
+ *
3821
+ * @param teamName The workspace display name (stored as `team_name` for
3822
+ * backward compatibility — a cloud IS a workspace with members).
3725
3823
  */
3726
3824
  register(cloudUrl: string, email: string, name: string, teamName: string): Promise<RegisterResponse>;
3727
3825
  redeemInvite(cloudUrl: string, inviteToken: string, email: string, name: string): Promise<RedeemResponse>;
@@ -3753,15 +3851,18 @@ declare class TeamsClient {
3753
3851
  };
3754
3852
  }>;
3755
3853
  /**
3756
- * Upgrade an already-connected cloud DB to a team DB. Two paths
3757
- * depending on the cloud URL's scheme:
3854
+ * Initialize a fresh cloud DB's owner: register the first member (who
3855
+ * becomes owner) so the cloud's members + per-table sharing surface
3856
+ * exists. This is NOT a "convert a cloud into a team" step — a cloud
3857
+ * workspace IS a workspace with members; this just bootstraps the owner
3858
+ * the first time a cloud is opened. The hosted server path is the only
3859
+ * supported one:
3758
3860
  *
3759
3861
  * - `http(s)://…` — POST to the cloud's `/api/auth/register` endpoint
3760
- * (`lattice serve --team-cloud` is fronting the Postgres).
3761
- * - `postgres(ql)://…` — drive the same INSERT sequence directly
3762
- * against the cloud Postgres via {@link registerDirectViaPostgres}.
3763
- * The HTTP path can't be used here because the browser's Fetch
3764
- * API refuses URLs with embedded credentials.
3862
+ * (a hosted `lattice serve` teams server is fronting the Postgres).
3863
+ * - `postgres(ql)://…` — rejected: direct postgres:// owner bootstrap
3864
+ * is deprecated. Row-level security is enforced by the hosted server,
3865
+ * so it is the only supported connection method for new workspaces.
3765
3866
  *
3766
3867
  * On success writes the bearer token to `~/.lattice/keys/<label>.token`
3767
3868
  * **and** persists the local `__lattice_team_connections` row so the
@@ -3771,9 +3872,10 @@ declare class TeamsClient {
3771
3872
  * the token file, leaving GUI authenticated calls with no
3772
3873
  * `cloud_url` + `my_user_id` + `api_token_encrypted` row to read.
3773
3874
  */
3774
- upgradeToTeamCloud(opts: {
3875
+ registerCloudOwner(opts: {
3775
3876
  label: string;
3776
3877
  cloudUrl: string;
3878
+ /** Workspace display name (stored as `team_name` for backward compatibility). */
3777
3879
  teamName: string;
3778
3880
  email: string;
3779
3881
  displayName: string;
@@ -4113,9 +4215,13 @@ declare function isPostgresUrl(url: string): boolean;
4113
4215
  * - Refuses if any non-deleted user already exists on the cloud.
4114
4216
  * - Refuses if the `__lattice_team_identity` singleton already exists.
4115
4217
  *
4116
- * On success returns the same shape the HTTP route returns so the
4117
- * caller (`TeamsClient.upgradeToTeamCloud`) can use either path
4118
- * interchangeably.
4218
+ * On success returns the same shape the HTTP route returns so a
4219
+ * direct-postgres register can mirror the hosted register path.
4220
+ *
4221
+ * @deprecated Since 2.2 — new direct registrations are rejected (a direct
4222
+ * connection bypasses row-level security). Retained only for the
4223
+ * grandfathered existing-connection path; will be removed in 3.0. Create or
4224
+ * join new workspaces through a hosted Teams server (an `http(s)://` URL).
4119
4225
  */
4120
4226
  declare function registerDirectViaPostgres(cloudUrl: string, email: string, name: string, teamName: string): Promise<DirectRegisterResult>;
4121
4227
 
package/dist/index.d.ts CHANGED
@@ -1969,6 +1969,76 @@ declare class Lattice {
1969
1969
  */
1970
1970
  search(table: string, query: string, opts?: SearchOptions): Promise<SearchResult[]>;
1971
1971
  query(table: string, opts?: QueryOptions): Promise<Row[]>;
1972
+ /**
1973
+ * Row-level-security list read for Lattice Teams (2.2). Returns only the
1974
+ * rows of `table` that `userId` may see in team `teamId`, evaluated
1975
+ * entirely in SQL (indexed, bounded — never "load every row then filter
1976
+ * in JS"). A row is visible iff it has a `__lattice_row_acl` entry owned by
1977
+ * the user or marked 'everyone', or a 'custom' entry with a matching
1978
+ * `__lattice_row_grants` row, OR it has no ACL entry at all and the caller
1979
+ * passes `noAclVisible` (the table default is 'everyone', or the user owns
1980
+ * the table — the pre-2.2 / never-narrowed case). Soft-deleted rows are
1981
+ * excluded by default; results reuse the same decrypt path as `query()`.
1982
+ *
1983
+ * The ACL predicate joins on the table's primary-key column cast to TEXT
1984
+ * (ACL pks are stored as TEXT), so it is correct regardless of the user
1985
+ * table's pk type and works on both SQLite and Postgres. The teams layer's
1986
+ * `listVisibleRows` (src/teams/row-access.ts) is the intended caller.
1987
+ */
1988
+ queryVisible(table: string, opts: {
1989
+ teamId: string;
1990
+ userId: string;
1991
+ /**
1992
+ * Whether rows with NO `__lattice_row_acl` entry are visible to this
1993
+ * user — true when the table default is 'everyone' OR the user owns the
1994
+ * table (the pre-2.2 / never-narrowed case). Resolved by the teams layer
1995
+ * (`listVisibleRows`); defaults to false, i.e. only rows with an explicit
1996
+ * ACL entry granting access are returned.
1997
+ */
1998
+ noAclVisible?: boolean;
1999
+ /** Soft-delete handling: 'exclude' (default), 'only' (trash), 'any'. */
2000
+ deleted?: 'exclude' | 'only' | 'any';
2001
+ limit?: number;
2002
+ offset?: number;
2003
+ orderBy?: string;
2004
+ orderDir?: 'asc' | 'desc';
2005
+ }): Promise<Row[]>;
2006
+ /**
2007
+ * Visible-row counts for MANY tables in a single round-trip, using the same
2008
+ * ACL predicate as {@link queryVisible} — so dashboard tiles agree with what
2009
+ * the rows view lists and a physical count never reveals the existence or
2010
+ * volume of rows the user can't see. One aggregated
2011
+ * `SELECT (SELECT COUNT(*) …) AS c0, …` statement (no per-table fan-out, so
2012
+ * a session pooler with few slots survives concurrent refreshes), capped at
2013
+ * 50 tables per pass; overflow is logged and skipped (no silent truncation)
2014
+ * and those tables count as absent — the caller renders "—". Soft-deleted
2015
+ * rows are excluded wherever the table carries `deleted_at`, matching the
2016
+ * default rows view.
2017
+ */
2018
+ countVisibleMany(specs: {
2019
+ table: string;
2020
+ noAclVisible: boolean;
2021
+ }[], opts: {
2022
+ teamId: string;
2023
+ userId: string;
2024
+ }): Promise<Map<string, number>>;
2025
+ /**
2026
+ * Hosted-sync change-log pull, filtered per recipient for 2.2 row-level
2027
+ * security (the hosted server's sole enforcement mechanism). Returns
2028
+ * `__lattice_change_log` rows with seq > `since` for team `teamId` that
2029
+ * `userId` is permitted to receive:
2030
+ * - targeted envelopes (`recipient_user_id = userId`), plus
2031
+ * - broadcast envelopes (`recipient_user_id IS NULL`) that are either
2032
+ * table-level (`pk IS NULL` — schema / unshare, delivered to all) or
2033
+ * whose row is currently visible to the user via `__lattice_row_acl` /
2034
+ * `__lattice_row_grants` (or has no ACL entry and the table defaults to
2035
+ * 'everyone').
2036
+ * Ordered by seq, capped at `limit`. Raw SQL because the predicate needs
2037
+ * OR / EXISTS that the `query()` API can't express; bounded by the seq
2038
+ * window and indexed ACL point-lookups. Mirrors {@link queryVisible}'s
2039
+ * visibility logic so a member never pulls the bytes of a row they can't see.
2040
+ */
2041
+ listChangesForRecipient(teamId: string, since: number, userId: string, limit: number): Promise<Row[]>;
1972
2042
  count(table: string, opts?: CountOptions): Promise<number>;
1973
2043
  render(outputDir: string): Promise<RenderResult>;
1974
2044
  sync(outputDir: string): Promise<SyncResult>;
@@ -2041,6 +2111,31 @@ declare class Lattice {
2041
2111
  * - `Record` → matches every PK column; all must be present in the object.
2042
2112
  */
2043
2113
  private _pkWhere;
2114
+ private static readonly _PK_SEP;
2115
+ /**
2116
+ * The primary-key columns of `table` that PHYSICALLY exist, in declared
2117
+ * order. Empty when the table has no Lattice-addressable key — e.g. a table
2118
+ * reached via raw SQL whose PK metadata defaulted to `['id']` but that has
2119
+ * no `id` column. Callers treat an empty result as "unkeyable" (no per-row
2120
+ * ACL is possible, so the row-perm SQL must not reference a pk column).
2121
+ */
2122
+ private _resolvedPkCols;
2123
+ /** Canonical ACL / change-log `pk` string for a row. Matches {@link _pkSqlExpr}. */
2124
+ private _serializeRowPk;
2125
+ /**
2126
+ * Canonical `pk` string for a {@link PkLookup} used by update/delete, so a
2127
+ * row addressed by lookup keys its change-log entry identically to the way
2128
+ * {@link _serializeRowPk} keyed it at insert time.
2129
+ */
2130
+ private _serializePkLookup;
2131
+ /**
2132
+ * SQL expression reconstructing {@link _serializeRowPk} from a row aliased
2133
+ * `t`. Returns null when the table is unkeyable (no pk columns present) — the
2134
+ * caller must then avoid referencing a pk column at all. Dialect-aware tab
2135
+ * separator: SQLite `char(9)`, Postgres `chr(9)` (both = U+0009), matching
2136
+ * {@link _PK_SEP}.
2137
+ */
2138
+ private _pkSqlExpr;
2044
2139
  /**
2045
2140
  * Convert Filter objects into SQL clause strings and bound params.
2046
2141
  * An `in` filter with an empty array is silently ignored (produces no clause).
@@ -3722,6 +3817,9 @@ declare class TeamsClient {
3722
3817
  * members join via `redeemInvite`). Returns the new user + bearer
3723
3818
  * token + team summary so the caller can immediately save a
3724
3819
  * connection.
3820
+ *
3821
+ * @param teamName The workspace display name (stored as `team_name` for
3822
+ * backward compatibility — a cloud IS a workspace with members).
3725
3823
  */
3726
3824
  register(cloudUrl: string, email: string, name: string, teamName: string): Promise<RegisterResponse>;
3727
3825
  redeemInvite(cloudUrl: string, inviteToken: string, email: string, name: string): Promise<RedeemResponse>;
@@ -3753,15 +3851,18 @@ declare class TeamsClient {
3753
3851
  };
3754
3852
  }>;
3755
3853
  /**
3756
- * Upgrade an already-connected cloud DB to a team DB. Two paths
3757
- * depending on the cloud URL's scheme:
3854
+ * Initialize a fresh cloud DB's owner: register the first member (who
3855
+ * becomes owner) so the cloud's members + per-table sharing surface
3856
+ * exists. This is NOT a "convert a cloud into a team" step — a cloud
3857
+ * workspace IS a workspace with members; this just bootstraps the owner
3858
+ * the first time a cloud is opened. The hosted server path is the only
3859
+ * supported one:
3758
3860
  *
3759
3861
  * - `http(s)://…` — POST to the cloud's `/api/auth/register` endpoint
3760
- * (`lattice serve --team-cloud` is fronting the Postgres).
3761
- * - `postgres(ql)://…` — drive the same INSERT sequence directly
3762
- * against the cloud Postgres via {@link registerDirectViaPostgres}.
3763
- * The HTTP path can't be used here because the browser's Fetch
3764
- * API refuses URLs with embedded credentials.
3862
+ * (a hosted `lattice serve` teams server is fronting the Postgres).
3863
+ * - `postgres(ql)://…` — rejected: direct postgres:// owner bootstrap
3864
+ * is deprecated. Row-level security is enforced by the hosted server,
3865
+ * so it is the only supported connection method for new workspaces.
3765
3866
  *
3766
3867
  * On success writes the bearer token to `~/.lattice/keys/<label>.token`
3767
3868
  * **and** persists the local `__lattice_team_connections` row so the
@@ -3771,9 +3872,10 @@ declare class TeamsClient {
3771
3872
  * the token file, leaving GUI authenticated calls with no
3772
3873
  * `cloud_url` + `my_user_id` + `api_token_encrypted` row to read.
3773
3874
  */
3774
- upgradeToTeamCloud(opts: {
3875
+ registerCloudOwner(opts: {
3775
3876
  label: string;
3776
3877
  cloudUrl: string;
3878
+ /** Workspace display name (stored as `team_name` for backward compatibility). */
3777
3879
  teamName: string;
3778
3880
  email: string;
3779
3881
  displayName: string;
@@ -4113,9 +4215,13 @@ declare function isPostgresUrl(url: string): boolean;
4113
4215
  * - Refuses if any non-deleted user already exists on the cloud.
4114
4216
  * - Refuses if the `__lattice_team_identity` singleton already exists.
4115
4217
  *
4116
- * On success returns the same shape the HTTP route returns so the
4117
- * caller (`TeamsClient.upgradeToTeamCloud`) can use either path
4118
- * interchangeably.
4218
+ * On success returns the same shape the HTTP route returns so a
4219
+ * direct-postgres register can mirror the hosted register path.
4220
+ *
4221
+ * @deprecated Since 2.2 — new direct registrations are rejected (a direct
4222
+ * connection bypasses row-level security). Retained only for the
4223
+ * grandfathered existing-connection path; will be removed in 3.0. Create or
4224
+ * join new workspaces through a hosted Teams server (an `http(s)://` URL).
4119
4225
  */
4120
4226
  declare function registerDirectViaPostgres(cloudUrl: string, email: string, name: string, teamName: string): Promise<DirectRegisterResult>;
4121
4227