feat: Expand zero-sync schema with users, teams, and Steam integration (#275)

## Description
<!-- Briefly describe the purpose and scope of your changes -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Expanded data model to include users, Steam accounts, teams, members,
and friends lists for richer user and team management.
- Introduced detailed relationships and row-level permissions for
enhanced access control.

- **Chores**
  - Updated dependency version for improved compatibility.
- Adjusted environment variables and configuration for improved
performance and reliability.
- Updated development scripts for clearer SQL permissions generation and
workflow separation.
  - Enhanced .gitignore to exclude SQL files from version control.

- **Refactor**
- Restructured schema and permissions logic for greater flexibility and
security.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Wanjohi
2025-05-09 06:58:13 +03:00
committed by GitHub
parent 1923cdf2a3
commit c250fd557c
7 changed files with 395 additions and 103 deletions

View File

@@ -1 +1,2 @@
sync-replica*
sync-replica*
*.sql

View File

@@ -1 +0,0 @@
UPDATE zero.permissions SET permissions = '{"tables":{"member":{"row":{"select":[["allow",{"type":"correlatedSubquery","related":{"system":"permissions","correlation":{"parentField":["team_id"],"childField":["team_id"]},"subquery":{"table":"member","alias":"zsubq_members","where":{"type":"simple","left":{"type":"column","name":"email"},"right":{"type":"static","anchor":"authData","field":"sub"},"op":"="},"orderBy":[["team_id","asc"],["id","asc"]]}},"op":"EXISTS"}]],"update":{}}},"team":{"row":{"select":[["allow",{"type":"correlatedSubquery","related":{"system":"permissions","correlation":{"parentField":["id"],"childField":["team_id"]},"subquery":{"table":"member","alias":"zsubq_members","where":{"type":"simple","left":{"type":"column","name":"email"},"right":{"type":"static","anchor":"authData","field":"sub"},"op":"="},"orderBy":[["team_id","asc"],["id","asc"]]}},"op":"EXISTS"}]],"update":{}}}}}';

View File

@@ -7,6 +7,7 @@
"@nestri/core": "*"
},
"scripts": {
"dev": "zero-deploy-permissions --output-format=sql --output-file=.permissions.sql && zero-deploy-permissions && zero-cache"
"dev": "zero-deploy-permissions && zero-cache",
"generate": "zero-deploy-permissions --output-format=sql --output-file=permissions.sql"
}
}

View File

@@ -1,60 +1,162 @@
import { type Limitations } from "@nestri/core/src/steam/steam.sql";
import {
json,
table,
string,
number,
string,
createSchema,
relationships,
definePermissions,
type ExpressionBuilder,
} from "@rocicorp/zero";
// TODO: Add Steam, and Machines here
const timestamps = {
time_created: number(),
time_deleted: number().optional(),
} as const;
const team = table("team")
// Table Definitions
const users = table("users")
.columns({
id: string(),
email: string(),
avatar_url: string().optional(),
last_login: number(),
name: string(),
polar_customer_id: string().optional(),
...timestamps
})
.primaryKey("id");
const steam_accounts = table("steam_accounts")
.columns({
steam_id: string(),
user_id: string(),
status: string(),
last_synced_at: number(),
real_name: string().optional(),
member_since: number(),
name: string(),
profile_url: string().optional(),
username: string(),
avatar_hash: string(),
limitations: json<Limitations>(),
...timestamps,
})
.primaryKey("steam_id");
const teams = table("teams")
.columns({
id: string(),
name: string(),
owner_id: string(),
invite_code: string(),
slug: string(),
plan_type: string<"Hosted" | "BYOG">(),
max_members: number(),
...timestamps,
})
.primaryKey("id");
const member = table("member")
const members = table("members")
.columns({
id: string(),
email: string(),
team_id: string(),
time_created: number(),
time_seen: number().optional(),
time_deleted: number().optional(),
user_id: string().optional(),
steam_id: string(),
role: string(),
...timestamps,
})
.primaryKey("team_id", "id");
.primaryKey("team_id", "steam_id");
export const schema = createSchema(1, {
tables: [team, member],
const friends_list = table("friends_list")
.columns({
steam_id: string(),
friend_steam_id: string(),
...timestamps,
})
.primaryKey("steam_id", "friend_steam_id");
// Schema and Relationships
export const schema = createSchema({
tables: [users, steam_accounts, teams, members, friends_list],
relationships: [
relationships(member, (r) => ({
team: r.one({
sourceField: ["team_id"],
destSchema: team,
relationships(steam_accounts, (r) => ({
user: r.one({
sourceField: ["user_id"],
destSchema: users,
destField: ["id"],
}),
memberEntries: r.many({
sourceField: ["steam_id"],
destSchema: members,
destField: ["steam_id"],
}),
friends: r.many({
sourceField: ["steam_id"],
destSchema: friends_list,
destField: ["steam_id"],
}),
friendOf: r.many({
sourceField: ["steam_id"],
destSchema: friends_list,
destField: ["friend_steam_id"],
}),
})),
relationships(users, (r) => ({
teams: r.many({
sourceField: ["id"],
destSchema: teams,
destField: ["owner_id"],
}),
members: r.many({
sourceField: ["team_id"],
destSchema: member,
sourceField: ["id"],
destSchema: members,
destField: ["user_id"],
}),
})),
relationships(teams, (r) => ({
owner: r.one({
sourceField: ["owner_id"],
destSchema: users,
destField: ["id"],
}),
steamAccount: r.one({
sourceField: ["owner_id"],
destSchema: steam_accounts,
destField: ["user_id"],
}),
members: r.many({
sourceField: ["id"],
destSchema: members,
destField: ["team_id"],
}),
})),
relationships(team, (r) => ({
members: r.many({
sourceField: ["id"],
destSchema: member,
destField: ["team_id"],
relationships(members, (r) => ({
team: r.one({
sourceField: ["team_id"],
destSchema: teams,
destField: ["id"],
}),
user: r.one({
sourceField: ["user_id"],
destSchema: users,
destField: ["id"],
}),
steamAccount: r.one({
sourceField: ["steam_id"],
destSchema: steam_accounts,
destField: ["steam_id"],
}),
})),
relationships(friends_list, (r) => ({
steam: r.one({
sourceField: ["steam_id"],
destSchema: steam_accounts,
destField: ["steam_id"],
}),
friend: r.one({
sourceField: ["friend_steam_id"],
destSchema: steam_accounts,
destField: ["steam_id"],
}),
})),
],
@@ -72,18 +174,41 @@ type Auth = {
export const permissions = definePermissions<Auth, Schema>(schema, () => {
return {
member: {
members: {
row: {
select: [
(auth, q) => q.exists("members", (u) => u.where("email", auth.sub)),
],
(auth: Auth, q: ExpressionBuilder<Schema, 'members'>) => q.exists("user", (u) => u.where("id", auth.sub)),
(auth: Auth, q: ExpressionBuilder<Schema, 'members'>) => q.exists("steamAccount", (u) => u.where("user_id", auth.sub)),
]
},
},
team: {
teams: {
row: {
select: [
(auth, q) => q.exists("members", (u) => u.where("email", auth.sub)),
],
(auth: Auth, q: ExpressionBuilder<Schema, 'teams'>) => q.exists("members", (u) => u.where("user_id", auth.sub)),
]
},
},
steam_accounts: {
row: {
select: [
(auth: Auth, q: ExpressionBuilder<Schema, 'steam_accounts'>) => q.exists("user", (u) => u.where("id", auth.sub)),
]
},
},
users: {
row: {
select: [
(auth: Auth, q: ExpressionBuilder<Schema, 'users'>) => q.cmp("id", "=", auth.sub),
]
},
},
friends_list: {
row: {
select: [
(auth: Auth, q: ExpressionBuilder<Schema, 'friends_list'>) => q.exists("steam", (u) => u.where("user_id", auth.sub)),
(auth: Auth, q: ExpressionBuilder<Schema, 'friends_list'>) => q.exists("friend", (u) => u.where("user_id", auth.sub)),
]
},
},
};