Add self-hosted PocketBase stack with auto-migrations
- docker-compose.yml: app + pocketbase services (PB internal to the network) - pocketbase/: pinned PocketBase image, superuser bootstrap via env vars - pb_migrations: production schema (collections, rules, public_users view) applied automatically on first boot - pocketbase/pb_schema.json: schema export kept as reference - README: full-stack deployment docs
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Initial PokeShare schema: extends the default `users` auth collection and
|
||||
* creates `public_users`, `mycards` and `friendships` with their API rules.
|
||||
* Mirrors the production schema (pb_schema.json export).
|
||||
* Applied automatically by `pocketbase serve` on first boot.
|
||||
*/
|
||||
migrate((app) => {
|
||||
// --- users (auth collection): extra profile fields, self-only access ---
|
||||
const users = app.findCollectionByNameOrId("users");
|
||||
users.fields.add(
|
||||
new Text({ name: "username", required: true, min: 3, max: 20 }),
|
||||
new Select({
|
||||
name: "theme_preference",
|
||||
values: ["System", "Light", "Dark"],
|
||||
maxSelect: 1,
|
||||
}),
|
||||
new Select({
|
||||
name: "visibility",
|
||||
values: ["Public", "Friends", "Private"],
|
||||
maxSelect: 1,
|
||||
}),
|
||||
);
|
||||
users.indexes.push(
|
||||
"CREATE UNIQUE INDEX idx_users_username ON users (username COLLATE NOCASE)",
|
||||
);
|
||||
users.listRule = "id = @request.auth.id";
|
||||
users.viewRule = "id = @request.auth.id";
|
||||
users.updateRule = "id = @request.auth.id";
|
||||
// No public registration — accounts are created from the admin UI (/_/)
|
||||
users.createRule = null;
|
||||
app.save(users);
|
||||
|
||||
// --- public_users: read-only view used by the friend search ---
|
||||
const publicUsers = new Collection({
|
||||
name: "public_users",
|
||||
type: "view",
|
||||
viewQuery: `SELECT id, username, name, visibility FROM users WHERE visibility != 'Private'`,
|
||||
listRule: '@request.auth.id != ""',
|
||||
viewRule: null,
|
||||
});
|
||||
app.save(publicUsers);
|
||||
|
||||
// --- mycards: one row per owned card ---
|
||||
// listRule enforces collection visibility at the database level:
|
||||
// owner, or anyone if the owner is Public, or accepted friends if Friends.
|
||||
const mycards = new Collection({
|
||||
name: "mycards",
|
||||
type: "base",
|
||||
fields: [
|
||||
{ name: "card_id", type: "text" },
|
||||
{ name: "card_name", type: "text" },
|
||||
{
|
||||
name: "user",
|
||||
type: "relation",
|
||||
collectionId: "_pb_users_auth_",
|
||||
maxSelect: 1,
|
||||
cascadeDelete: true,
|
||||
},
|
||||
{ name: "card_image", type: "url" },
|
||||
{ name: "variant", type: "text" },
|
||||
{ name: "quantity", type: "number" },
|
||||
{ name: "set_name", type: "text" },
|
||||
{ name: "notes", type: "text" },
|
||||
{ name: "condition", type: "text" },
|
||||
],
|
||||
listRule:
|
||||
"@request.auth.id = user || (user.visibility = 'Public') || (user.visibility = 'Friends' && ((@collection.friendships.requester = user && @collection.friendships.addressee = @request.auth.id && @collection.friendships.status = 'accepted') || (@collection.friendships.addressee = user && @collection.friendships.requester = @request.auth.id && @collection.friendships.status = 'accepted')))",
|
||||
viewRule:
|
||||
"@request.auth.id = user || (@collection.friendships.requester = user && @collection.friendships.addressee = @request.auth.id && @collection.friendships.status = 'accepted') || (@collection.friendships.addressee = user && @collection.friendships.requester = @request.auth.id && @collection.friendships.status = 'accepted')",
|
||||
createRule: '@request.auth.id != "" && user = @request.auth.id',
|
||||
updateRule: '@request.auth.id != "" && user = @request.auth.id',
|
||||
deleteRule: '@request.auth.id != "" && user = @request.auth.id',
|
||||
});
|
||||
app.save(mycards);
|
||||
|
||||
// --- friendships ---
|
||||
const friendships = new Collection({
|
||||
name: "friendships",
|
||||
type: "base",
|
||||
fields: [
|
||||
{
|
||||
name: "requester",
|
||||
type: "relation",
|
||||
collectionId: "_pb_users_auth_",
|
||||
maxSelect: 1,
|
||||
cascadeDelete: true,
|
||||
},
|
||||
{
|
||||
name: "addressee",
|
||||
type: "relation",
|
||||
collectionId: "_pb_users_auth_",
|
||||
maxSelect: 1,
|
||||
cascadeDelete: true,
|
||||
},
|
||||
{
|
||||
name: "status",
|
||||
type: "select",
|
||||
values: ["accepted", "pending", "declined"],
|
||||
maxSelect: 1,
|
||||
},
|
||||
],
|
||||
listRule: "@request.auth.id = requester || @request.auth.id = addressee",
|
||||
viewRule: "@request.auth.id = requester || @request.auth.id = addressee",
|
||||
createRule: "@request.auth.id = requester && addressee != requester",
|
||||
updateRule: "@request.auth.id = addressee || @request.auth.id = requester",
|
||||
deleteRule: "@request.auth.id = addressee || @request.auth.id = requester",
|
||||
});
|
||||
app.save(friendships);
|
||||
}, (app) => {
|
||||
// Down: drop the custom collections. The added `users` fields are left in
|
||||
// place — removing them would destroy data and they are harmless.
|
||||
for (const name of ["friendships", "mycards", "public_users"]) {
|
||||
try {
|
||||
app.delete(app.findCollectionByNameOrId(name));
|
||||
} catch {
|
||||
// already missing
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user