Compare commits

...
5 Commits
Author SHA1 Message Date
Rafał Tyński 0a5ef715eb feat(sql): add demo schema additions and synthetic seed data
Add additive schema for demo (user.display_name, innovation.tags, FLEXIBLE free-form fields) and a fully synthetic seed with organizations, personas, problems, submissions, innovations, knowledge items, grant calls, ideas and tester feedback. Verified on SurrealDB 3.2.4: migrations apply cleanly and are idempotent.
2026-10-04 01:24:18 +02:00
Rafał Tyński 6df1027699 feat(sql): add SurrealDB schema and taxonomy seed migrations
Add an idempotent SurrealDB 3.x schema for all domain entities with indexes (HNSW vector and full-text), record access and permissions, plus a taxonomy seed with the 8 social challenges, grouped tags and sample locations. Verified on SurrealDB 3.2.4: migrations apply cleanly, re-run is idempotent and CRUD works.
2026-10-04 01:11:33 +02:00
Rafał Tyński 67ec85b6cc docs(readme): document CI/CD, environments and observability 2026-10-04 00:57:40 +02:00
Rafał Tyński 70fb592cf7 chore(config): add backend environment variable example 2026-10-04 00:57:40 +02:00
Rafał Tyński 6c78351a95 ci: add disabled Gitea Actions CI/CD workflows
Add CI and CD workflows for backend, frontend and image publishing. The workflows are fully commented out because no Gitea Actions runner is attached; uncomment once a runner is registered.
2026-10-04 00:57:40 +02:00
9 changed files with 700 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# TarninaHub — backend runtime configuration example.
# Copy to `.env` (not tracked) or provide as environment variables.
# Never commit real secrets.
ASPNETCORE_ENVIRONMENT=Production
ASPNETCORE_URLS=http://+:8080
# SurrealDB — embedded RocksDB by default.
SurrealDb__Namespace=hubmi
SurrealDb__Database=hubmi
SurrealDb__DataDirectory=data
# SurrealDB — remote mode (optional; leave DataDirectory unused when set).
# SurrealDb__RemoteUrl=ws://127.0.0.1:8010/rpc
# SurrealDb__Username=
# SurrealDb__Password=
# LLM (F-8, matchmaking) — runtime secret only.
# LLM__ApiKey=
# Observability (optional).
# Logging__LogLevel__Default=Information
# OTEL_EXPORTER_OTLP_ENDPOINT=
# OTEL_SERVICE_NAME=tarninahub
+50
View File
@@ -0,0 +1,50 @@
# CD workflow — DISABLED.
# No Gitea Actions runner is attached to this repository yet, so the workflow
# below is fully commented out to avoid failing runs. Uncomment everything once
# a runner is registered (repository Settings -> Actions -> Runners).
# name: CD
#
# on:
# push:
# branches: [main]
# tags: ["v*"]
#
# jobs:
# publish:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: Read version
# id: version
# run: |
# VERSION=$(sed -n 's:.*<Version>\(.*\)</Version>.*:\1:p' backend/TarninaHub.csproj | head -1)
# echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# - name: Log in to registry
# run: echo "${{ secrets.GITEA_TOKEN }}" | docker login git.rtynski.pl -u "${{ github.actor }}" --password-stdin
# - name: Build image
# run: |
# docker build \
# --build-arg APP_VERSION=${{ steps.version.outputs.version }} \
# -t git.rtynski.pl/rtynski/hy2026:${{ steps.version.outputs.version }} \
# -t git.rtynski.pl/rtynski/hy2026:latest .
# - name: Push image
# run: |
# docker push git.rtynski.pl/rtynski/hy2026:${{ steps.version.outputs.version }}
# docker push git.rtynski.pl/rtynski/hy2026:latest
#
# # Deploy demo runs after the image is published from `main`.
# # Configure DEPLOY_HOST / DEPLOY_USER / DEPLOY_SSH_KEY as CI secrets.
# # deploy:
# # needs: publish
# # if: github.ref == 'refs/heads/main'
# # runs-on: ubuntu-latest
# # steps:
# # - name: Deploy over SSH
# # run: |
# # echo "${{ secrets.DEPLOY_SSH_KEY }}" > key && chmod 600 key
# # ssh -i key -o StrictHostKeyChecking=no \
# # "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \
# # "cd /opt/tarninahub && APP_VERSION=latest docker compose pull && docker compose up -d"
# # - name: Smoke test
# # run: curl -fsS "http://${{ secrets.DEPLOY_HOST }}:8075/api/health"
+46
View File
@@ -0,0 +1,46 @@
# CI workflow — DISABLED.
# No Gitea Actions runner is attached to this repository yet, so the workflow
# below is fully commented out to avoid failing runs. Uncomment everything once
# a runner is registered (repository Settings -> Actions -> Runners).
# name: CI
#
# on:
# push:
# branches: [main]
# pull_request:
# branches: [main]
#
# jobs:
# backend:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-dotnet@v4
# with:
# dotnet-version: "10.0.x"
# - name: Restore
# run: dotnet restore backend/TarninaHub.csproj
# - name: Build
# run: dotnet build backend/TarninaHub.csproj --configuration Release --no-restore
# - name: Format check
# run: dotnet format backend/TarninaHub.csproj --verify-no-changes
# # Tests: enabled once test projects are added (`dotnet test`).
#
# frontend:
# runs-on: ubuntu-latest
# defaults:
# run:
# working-directory: client
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-node@v4
# with:
# node-version: "22"
# cache: npm
# cache-dependency-path: client/package-lock.json
# - name: Install
# run: npm ci
# - name: Build (typecheck + bundle)
# run: npm run build
# # Lint: enabled once ESLint is wired up (`npm run lint`).
+25
View File
@@ -84,6 +84,31 @@ kopiuje go do `backend/wwwroot`, buduje backend, buduje obraz Docker i wypycha g
do `git.rtynski.pl/rtynski/hy2026` jako `<wersja>` oraz `latest`. Nadpisanie do `git.rtynski.pl/rtynski/hy2026` jako `<wersja>` oraz `latest`. Nadpisanie
sprawdzenia: `-Force`. sprawdzenia: `-Force`.
## CI/CD
Pipeline działa w **Gitea Actions** (`.gitea/workflows/`):
> **Uwaga:** workflow są zakomentowane w całości — do repozytorium nie jest podpięty runner Gitea. Odkomentuj po zarejestrowaniu runera (Settings → Actions → Runners).
- `ci.yml` — na `push`/PR do `main`: build backendu (.NET 10), `dotnet format --verify-no-changes` oraz build klienta (Node 22, `vue-tsc` + bundle).
- `cd.yml` — na `main`/tag `v*`: build obrazu i push do `git.rtynski.pl/rtynski/hy2026` (`<wersja>` + `latest`); deploy demo opcjonalnie przez sekrety CI.
Sekrety CI: `GITEA_TOKEN` (`write:package`), opcjonalnie `DEPLOY_HOST`/`DEPLOY_USER`/`DEPLOY_SSH_KEY`. Klucz `LLM__ApiKey` przekazujemy wyłącznie na runtime — nigdy do repo ani warstw obrazu.
## Środowiska i konfiguracja
- **dev** — `appsettings.Development.json`, dane w `backend/data`.
- **demo** — obraz z rejestru, `docker-compose.yaml.example`, port `TARNINAHUB_PORT` (8075), wersja `APP_VERSION`.
- **staging** (opcjonalne) — kandydat wydania przed demo.
Konfiguracja przez zmienne środowiskowe (`SurrealDb__*`, `LLM__ApiKey`, `OTEL_*`); szablon: [`.env.example`](.env.example).
## Observability
- Healthcheck: `GET /api/health` (używany też przez Docker healthcheck).
- Logi strukturalne (JSON) do `stdout`/`stderr`, z correlation ID.
- Docelowo: `/metrics` (Prometheus) i OpenTelemetry (traces).
## Struktura ## Struktura
``` ```
+339
View File
@@ -0,0 +1,339 @@
-- TarninaHub schema (SurrealDB 3.x) — migration 0001
-- Idempotent: uses IF NOT EXISTS, safe to re-run. Backend database is the
-- source of truth; the frontend (WASM) applies the same subset of definitions.
-- ---------------------------------------------------------------------------
-- Reference / dictionaries
-- ---------------------------------------------------------------------------
DEFINE TABLE IF NOT EXISTS social_challenge SCHEMAFULL
PERMISSIONS FOR select FULL, FOR create, update, delete NONE;
DEFINE FIELD IF NOT EXISTS name ON TABLE social_challenge TYPE string ASSERT string::len($value) > 0;
DEFINE FIELD IF NOT EXISTS description ON TABLE social_challenge TYPE string DEFAULT "";
DEFINE FIELD IF NOT EXISTS order ON TABLE social_challenge TYPE int DEFAULT 0;
DEFINE INDEX IF NOT EXISTS social_challenge_name ON TABLE social_challenge FIELDS name UNIQUE;
DEFINE TABLE IF NOT EXISTS tag SCHEMAFULL
PERMISSIONS FOR select FULL, FOR create, update, delete NONE;
DEFINE FIELD IF NOT EXISTS name ON TABLE tag TYPE string ASSERT string::len($value) > 0;
DEFINE FIELD IF NOT EXISTS group ON TABLE tag TYPE string
ASSERT $value IN ['audience', 'topic', 'stage', 'sector', 'cross'];
DEFINE INDEX IF NOT EXISTS tag_name_group ON TABLE tag FIELDS name, group UNIQUE;
DEFINE TABLE IF NOT EXISTS location SCHEMAFULL
PERMISSIONS FOR select FULL, FOR create, update, delete NONE;
DEFINE FIELD IF NOT EXISTS name ON TABLE location TYPE string;
DEFINE FIELD IF NOT EXISTS kind ON TABLE location TYPE string
ASSERT $value IN ['gmina', 'powiat', 'wojewodztwo'];
DEFINE FIELD IF NOT EXISTS teryt ON TABLE location TYPE option<string>;
DEFINE FIELD IF NOT EXISTS geometry ON TABLE location TYPE option<geometry>;
DEFINE FIELD IF NOT EXISTS parent ON TABLE location TYPE option<record<location>>;
DEFINE INDEX IF NOT EXISTS location_teryt ON TABLE location FIELDS teryt UNIQUE;
DEFINE TABLE IF NOT EXISTS organization SCHEMAFULL
PERMISSIONS FOR select FULL,
FOR create, update WHERE $auth.role IN ['operator', 'admin'],
FOR delete NONE;
DEFINE FIELD IF NOT EXISTS name ON TABLE organization TYPE string;
DEFINE FIELD IF NOT EXISTS type ON TABLE organization TYPE string
ASSERT $value IN ['ngo', 'jst', 'institution', 'company'];
DEFINE FIELD IF NOT EXISTS location ON TABLE organization TYPE option<record<location>>;
DEFINE FIELD IF NOT EXISTS contact ON TABLE organization TYPE option<object>;
-- ---------------------------------------------------------------------------
-- Identity (shared across departments)
-- ---------------------------------------------------------------------------
DEFINE TABLE IF NOT EXISTS user SCHEMAFULL
PERMISSIONS
FOR select WHERE id = $auth.id OR $auth.role IN ['operator', 'manager', 'admin'],
FOR create, update WHERE id = $auth.id,
FOR delete NONE;
DEFINE FIELD IF NOT EXISTS phone ON TABLE user TYPE string;
DEFINE FIELD IF NOT EXISTS email ON TABLE user TYPE option<string>;
DEFINE FIELD IF NOT EXISTS role ON TABLE user TYPE string
ASSERT $value IN ['user', 'jst', 'operator', 'manager', 'admin'];
DEFINE FIELD IF NOT EXISTS organization ON TABLE user TYPE option<record<organization>>;
DEFINE FIELD IF NOT EXISTS password ON TABLE user TYPE string;
DEFINE FIELD IF NOT EXISTS updated_at ON TABLE user TYPE option<datetime> DEFAULT time::now();
DEFINE FIELD IF NOT EXISTS version ON TABLE user TYPE int DEFAULT 1;
DEFINE FIELD IF NOT EXISTS deleted_at ON TABLE user TYPE option<datetime>;
DEFINE INDEX IF NOT EXISTS user_phone ON TABLE user FIELDS phone UNIQUE;
DEFINE INDEX IF NOT EXISTS user_email ON TABLE user FIELDS email UNIQUE;
-- Record access: users sign up / sign in and receive a JWT.
DEFINE ACCESS IF NOT EXISTS account ON DATABASE TYPE RECORD
SIGNUP ( CREATE user SET phone = $phone, email = $email, password = crypto::argon2::generate($password), role = 'user' )
SIGNIN ( SELECT * FROM user WHERE phone = $phone AND crypto::argon2::compare(password, $password) )
DURATION FOR TOKEN 15m, FOR SESSION 12h;
-- ---------------------------------------------------------------------------
-- Matchmaking (F-1): problems, submissions, innovations
-- ---------------------------------------------------------------------------
DEFINE TABLE IF NOT EXISTS problem SCHEMAFULL
PERMISSIONS
FOR select WHERE author = $auth.id OR visibility != 'private' OR $auth.role IN ['operator', 'manager', 'admin'],
FOR create, update WHERE author = $auth.id OR $auth.role IN ['operator', 'admin'],
FOR delete WHERE author = $auth.id OR $auth.role = 'admin';
DEFINE FIELD IF NOT EXISTS title ON TABLE problem TYPE string;
DEFINE FIELD IF NOT EXISTS description ON TABLE problem TYPE string;
DEFINE FIELD IF NOT EXISTS challenge ON TABLE problem TYPE record<social_challenge>;
DEFINE FIELD IF NOT EXISTS location ON TABLE problem TYPE record<location>;
DEFINE FIELD IF NOT EXISTS author ON TABLE problem TYPE record<user>;
DEFINE FIELD IF NOT EXISTS visibility ON TABLE problem TYPE string
ASSERT $value IN ['private', 'shared', 'public'] DEFAULT 'private';
DEFINE FIELD IF NOT EXISTS tags ON TABLE problem TYPE array<record<tag>> DEFAULT [];
DEFINE FIELD IF NOT EXISTS updated_at ON TABLE problem TYPE option<datetime> DEFAULT time::now();
DEFINE FIELD IF NOT EXISTS version ON TABLE problem TYPE int DEFAULT 1;
DEFINE FIELD IF NOT EXISTS deleted_at ON TABLE problem TYPE option<datetime>;
DEFINE INDEX IF NOT EXISTS problem_challenge ON TABLE problem FIELDS challenge;
DEFINE INDEX IF NOT EXISTS problem_location ON TABLE problem FIELDS location;
DEFINE TABLE IF NOT EXISTS submission SCHEMAFULL
PERMISSIONS
FOR select WHERE submitted_by = $auth.id OR $auth.role IN ['operator', 'admin'],
FOR create WHERE submitted_by = $auth.id,
FOR update WHERE $auth.role IN ['operator', 'admin'],
FOR delete NONE;
DEFINE FIELD IF NOT EXISTS problem ON TABLE submission TYPE record<problem>;
DEFINE FIELD IF NOT EXISTS submitted_by ON TABLE submission TYPE record<user>;
DEFINE FIELD IF NOT EXISTS status ON TABLE submission TYPE string
ASSERT $value IN ['new', 'triaged', 'matched', 'in_progress', 'closed', 'rejected'] DEFAULT 'new';
DEFINE FIELD IF NOT EXISTS keywords ON TABLE submission TYPE array<string> DEFAULT [];
DEFINE FIELD IF NOT EXISTS created_at ON TABLE submission TYPE datetime DEFAULT time::now();
DEFINE FIELD IF NOT EXISTS updated_at ON TABLE submission TYPE option<datetime> DEFAULT time::now();
DEFINE FIELD IF NOT EXISTS version ON TABLE submission TYPE int DEFAULT 1;
DEFINE FIELD IF NOT EXISTS deleted_at ON TABLE submission TYPE option<datetime>;
DEFINE INDEX IF NOT EXISTS submission_status ON TABLE submission FIELDS status;
DEFINE INDEX IF NOT EXISTS submission_problem ON TABLE submission FIELDS problem;
DEFINE TABLE IF NOT EXISTS innovation SCHEMAFULL
PERMISSIONS FOR select FULL,
FOR create, update WHERE $auth.role IN ['operator', 'manager', 'admin'],
FOR delete NONE;
DEFINE FIELD IF NOT EXISTS title ON TABLE innovation TYPE string;
DEFINE FIELD IF NOT EXISTS summary ON TABLE innovation TYPE string DEFAULT "";
DEFINE FIELD IF NOT EXISTS description ON TABLE innovation TYPE string DEFAULT "";
DEFINE FIELD IF NOT EXISTS challenge ON TABLE innovation TYPE record<social_challenge>;
DEFINE FIELD IF NOT EXISTS stage ON TABLE innovation TYPE string
ASSERT $value IN ['idea', 'pilot', 'scaling', 'service'] DEFAULT 'idea';
DEFINE FIELD IF NOT EXISTS cost ON TABLE innovation TYPE option<string>;
DEFINE FIELD IF NOT EXISTS contact ON TABLE innovation TYPE option<object>;
DEFINE FIELD IF NOT EXISTS media ON TABLE innovation TYPE array<object> DEFAULT [];
DEFINE FIELD IF NOT EXISTS embedding ON TABLE innovation TYPE option<array<float>>;
DEFINE FIELD IF NOT EXISTS published ON TABLE innovation TYPE bool DEFAULT false;
DEFINE FIELD IF NOT EXISTS updated_at ON TABLE innovation TYPE option<datetime> DEFAULT time::now();
DEFINE FIELD IF NOT EXISTS version ON TABLE innovation TYPE int DEFAULT 1;
DEFINE INDEX IF NOT EXISTS innovation_challenge ON TABLE innovation FIELDS challenge;
DEFINE INDEX IF NOT EXISTS innovation_embedding ON TABLE innovation FIELDS embedding
HNSW DIMENSION 1536 DIST COSINE TYPE F32;
-- ---------------------------------------------------------------------------
-- Idea creator (F-4)
-- ---------------------------------------------------------------------------
DEFINE TABLE IF NOT EXISTS idea SCHEMAFULL
PERMISSIONS
FOR select WHERE author = $auth.id OR $auth.role IN ['manager', 'operator', 'admin'],
FOR create, update WHERE author = $auth.id,
FOR delete WHERE author = $auth.id;
DEFINE FIELD IF NOT EXISTS title ON TABLE idea TYPE string;
DEFINE FIELD IF NOT EXISTS description ON TABLE idea TYPE string DEFAULT "";
DEFINE FIELD IF NOT EXISTS author ON TABLE idea TYPE record<user>;
DEFINE FIELD IF NOT EXISTS status ON TABLE idea TYPE string
ASSERT $value IN ['draft', 'submitted', 'in_review', 'accepted', 'rejected'] DEFAULT 'draft';
DEFINE FIELD IF NOT EXISTS updated_at ON TABLE idea TYPE option<datetime> DEFAULT time::now();
DEFINE FIELD IF NOT EXISTS version ON TABLE idea TYPE int DEFAULT 1;
DEFINE FIELD IF NOT EXISTS deleted_at ON TABLE idea TYPE option<datetime>;
DEFINE INDEX IF NOT EXISTS idea_author ON TABLE idea FIELDS author;
DEFINE INDEX IF NOT EXISTS idea_status ON TABLE idea FIELDS status;
DEFINE TABLE IF NOT EXISTS idea_card SCHEMAFULL
PERMISSIONS
FOR select WHERE idea.author = $auth.id OR $auth.role IN ['manager', 'operator', 'admin'],
FOR create, update WHERE idea.author = $auth.id,
FOR delete NONE;
DEFINE FIELD IF NOT EXISTS idea ON TABLE idea_card TYPE record<idea>;
DEFINE FIELD IF NOT EXISTS essence ON TABLE idea_card TYPE string DEFAULT "";
DEFINE FIELD IF NOT EXISTS audience ON TABLE idea_card TYPE string DEFAULT "";
DEFINE FIELD IF NOT EXISTS stage ON TABLE idea_card TYPE string
ASSERT $value IN ['idea', 'pilot', 'scaling', 'service'] DEFAULT 'idea';
DEFINE INDEX IF NOT EXISTS idea_card_idea ON TABLE idea_card FIELDS idea UNIQUE;
DEFINE TABLE IF NOT EXISTS grant_call SCHEMAFULL
PERMISSIONS FOR select FULL,
FOR create, update, delete WHERE $auth.role IN ['operator', 'admin'];
DEFINE FIELD IF NOT EXISTS name ON TABLE grant_call TYPE string;
DEFINE FIELD IF NOT EXISTS opens_at ON TABLE grant_call TYPE datetime;
DEFINE FIELD IF NOT EXISTS closes_at ON TABLE grant_call TYPE datetime;
DEFINE FIELD IF NOT EXISTS status ON TABLE grant_call TYPE string
ASSERT $value IN ['open', 'closed', 'planned'] DEFAULT 'planned';
DEFINE FIELD IF NOT EXISTS external_id ON TABLE grant_call TYPE option<string>;
DEFINE INDEX IF NOT EXISTS grant_call_status ON TABLE grant_call FIELDS status;
DEFINE TABLE IF NOT EXISTS application SCHEMAFULL
PERMISSIONS
FOR select WHERE idea.author = $auth.id OR $auth.role IN ['operator', 'admin'],
FOR create WHERE idea.author = $auth.id,
FOR update WHERE idea.author = $auth.id OR $auth.role IN ['operator', 'admin'],
FOR delete NONE;
DEFINE FIELD IF NOT EXISTS grant_call ON TABLE application TYPE record<grant_call>;
DEFINE FIELD IF NOT EXISTS idea ON TABLE application TYPE record<idea>;
DEFINE FIELD IF NOT EXISTS content ON TABLE application TYPE object DEFAULT {};
DEFINE FIELD IF NOT EXISTS status ON TABLE application TYPE string
ASSERT $value IN ['draft', 'submitted', 'accepted', 'rejected'] DEFAULT 'draft';
DEFINE FIELD IF NOT EXISTS created_at ON TABLE application TYPE datetime DEFAULT time::now();
-- ---------------------------------------------------------------------------
-- Tester (F-5)
-- ---------------------------------------------------------------------------
DEFINE TABLE IF NOT EXISTS test_feedback SCHEMAFULL
PERMISSIONS
FOR select WHERE author = $auth.id OR $auth.role IN ['manager', 'operator', 'admin'],
FOR create WHERE author = $auth.id,
FOR update, delete WHERE author = $auth.id;
DEFINE FIELD IF NOT EXISTS innovation ON TABLE test_feedback TYPE record<innovation>;
DEFINE FIELD IF NOT EXISTS author ON TABLE test_feedback TYPE record<user>;
DEFINE FIELD IF NOT EXISTS rating ON TABLE test_feedback TYPE int ASSERT $value >= 1 AND $value <= 5;
DEFINE FIELD IF NOT EXISTS comment ON TABLE test_feedback TYPE string DEFAULT "";
DEFINE FIELD IF NOT EXISTS improvements ON TABLE test_feedback TYPE string DEFAULT "";
DEFINE FIELD IF NOT EXISTS created_at ON TABLE test_feedback TYPE datetime DEFAULT time::now();
DEFINE INDEX IF NOT EXISTS test_feedback_innovation ON TABLE test_feedback FIELDS innovation;
-- ---------------------------------------------------------------------------
-- Communication (F-6)
-- ---------------------------------------------------------------------------
DEFINE TABLE IF NOT EXISTS partnership SCHEMAFULL
PERMISSIONS
FOR select WHERE initiator = $auth.id OR $auth.role IN ['operator', 'admin'],
FOR create, update WHERE initiator = $auth.id OR $auth.role IN ['operator', 'admin'],
FOR delete NONE;
DEFINE FIELD IF NOT EXISTS initiator ON TABLE partnership TYPE record<user>;
DEFINE FIELD IF NOT EXISTS subject ON TABLE partnership TYPE string;
DEFINE FIELD IF NOT EXISTS partners ON TABLE partnership TYPE array<record<organization>> DEFAULT [];
DEFINE FIELD IF NOT EXISTS status ON TABLE partnership TYPE string
ASSERT $value IN ['proposed', 'active', 'closed'] DEFAULT 'proposed';
DEFINE TABLE IF NOT EXISTS thread SCHEMAFULL
PERMISSIONS
FOR select WHERE $auth.id IN participants OR $auth.role IN ['operator', 'admin'],
FOR create WHERE $auth.id IN participants,
FOR update WHERE $auth.id IN participants,
FOR delete NONE;
DEFINE FIELD IF NOT EXISTS subject ON TABLE thread TYPE string;
DEFINE FIELD IF NOT EXISTS participants ON TABLE thread TYPE array<record<user>> DEFAULT [];
DEFINE FIELD IF NOT EXISTS created_at ON TABLE thread TYPE datetime DEFAULT time::now();
DEFINE TABLE IF NOT EXISTS message SCHEMAFULL
PERMISSIONS
FOR select WHERE thread.participants CONTAINS $auth.id OR $auth.role IN ['operator', 'admin'],
FOR create WHERE thread.participants CONTAINS $auth.id,
FOR update, delete WHERE author = $auth.id;
DEFINE FIELD IF NOT EXISTS thread ON TABLE message TYPE record<thread>;
DEFINE FIELD IF NOT EXISTS author ON TABLE message TYPE record<user>;
DEFINE FIELD IF NOT EXISTS body ON TABLE message TYPE string;
DEFINE FIELD IF NOT EXISTS created_at ON TABLE message TYPE datetime DEFAULT time::now();
DEFINE INDEX IF NOT EXISTS message_thread ON TABLE message FIELDS thread;
DEFINE TABLE IF NOT EXISTS notification SCHEMAFULL
PERMISSIONS
FOR select WHERE recipient = $auth.id,
FOR create WHERE $auth.role IN ['operator', 'admin'],
FOR update WHERE recipient = $auth.id,
FOR delete NONE;
DEFINE FIELD IF NOT EXISTS recipient ON TABLE notification TYPE record<user>;
DEFINE FIELD IF NOT EXISTS type ON TABLE notification TYPE string;
DEFINE FIELD IF NOT EXISTS payload ON TABLE notification TYPE object DEFAULT {};
DEFINE FIELD IF NOT EXISTS read_at ON TABLE notification TYPE option<datetime>;
DEFINE FIELD IF NOT EXISTS created_at ON TABLE notification TYPE datetime DEFAULT time::now();
DEFINE INDEX IF NOT EXISTS notification_recipient ON TABLE notification FIELDS recipient, read_at;
DEFINE TABLE IF NOT EXISTS comment SCHEMAFULL
PERMISSIONS
FOR select FULL,
FOR create WHERE author = $auth.id,
FOR update, delete WHERE author = $auth.id;
DEFINE FIELD IF NOT EXISTS target ON TABLE comment TYPE string;
DEFINE FIELD IF NOT EXISTS author ON TABLE comment TYPE record<user>;
DEFINE FIELD IF NOT EXISTS body ON TABLE comment TYPE string;
DEFINE FIELD IF NOT EXISTS created_at ON TABLE comment TYPE datetime DEFAULT time::now();
DEFINE INDEX IF NOT EXISTS comment_target ON TABLE comment FIELDS target;
DEFINE TABLE IF NOT EXISTS reaction SCHEMAFULL
PERMISSIONS
FOR select FULL,
FOR create, delete WHERE author = $auth.id,
FOR update NONE;
DEFINE FIELD IF NOT EXISTS target ON TABLE reaction TYPE string;
DEFINE FIELD IF NOT EXISTS author ON TABLE reaction TYPE record<user>;
DEFINE FIELD IF NOT EXISTS kind ON TABLE reaction TYPE string;
DEFINE INDEX IF NOT EXISTS reaction_target ON TABLE reaction FIELDS target;
-- ---------------------------------------------------------------------------
-- Knowledge base (F-3)
-- ---------------------------------------------------------------------------
DEFINE TABLE IF NOT EXISTS knowledge_item SCHEMAFULL
PERMISSIONS FOR select FULL,
FOR create, update WHERE $auth.role IN ['operator', 'admin'],
FOR delete WHERE $auth.role = 'admin';
DEFINE FIELD IF NOT EXISTS title ON TABLE knowledge_item TYPE string;
DEFINE FIELD IF NOT EXISTS kind ON TABLE knowledge_item TYPE string
ASSERT $value IN ['report', 'map', 'education', 'dataset', 'canvas', 'media'];
DEFINE FIELD IF NOT EXISTS challenge ON TABLE knowledge_item TYPE option<record<social_challenge>>;
DEFINE FIELD IF NOT EXISTS source_url ON TABLE knowledge_item TYPE option<string>;
DEFINE FIELD IF NOT EXISTS attachments ON TABLE knowledge_item TYPE array<object> DEFAULT [];
DEFINE FIELD IF NOT EXISTS published ON TABLE knowledge_item TYPE bool DEFAULT false;
DEFINE FIELD IF NOT EXISTS updated_at ON TABLE knowledge_item TYPE option<datetime> DEFAULT time::now();
DEFINE ANALYZER IF NOT EXISTS ascii TOKENIZERS class FILTERS lowercase, ascii;
DEFINE INDEX IF NOT EXISTS knowledge_item_ft ON TABLE knowledge_item FIELDS title
FULLTEXT ANALYZER ascii BM25;
-- ---------------------------------------------------------------------------
-- Documents, consent, audit (privacy)
-- ---------------------------------------------------------------------------
DEFINE TABLE IF NOT EXISTS document SCHEMAFULL
PERMISSIONS
FOR select WHERE owner = $auth.id OR $auth.role IN ['operator', 'admin'],
FOR create WHERE owner = $auth.id,
FOR update WHERE owner = $auth.id,
FOR delete NONE;
DEFINE FIELD IF NOT EXISTS title ON TABLE document TYPE string;
DEFINE FIELD IF NOT EXISTS doc_type ON TABLE document TYPE string;
DEFINE FIELD IF NOT EXISTS processing_purpose ON TABLE document TYPE string;
DEFINE FIELD IF NOT EXISTS scope ON TABLE document TYPE string
ASSERT $value IN ['local', 'partial', 'full', 'anonymized'] DEFAULT 'local';
DEFINE FIELD IF NOT EXISTS owner ON TABLE document TYPE record<user>;
DEFINE FIELD IF NOT EXISTS archived ON TABLE document TYPE bool DEFAULT false;
DEFINE FIELD IF NOT EXISTS related ON TABLE document TYPE option<string>;
DEFINE FIELD IF NOT EXISTS updated_at ON TABLE document TYPE option<datetime> DEFAULT time::now();
DEFINE FIELD IF NOT EXISTS version ON TABLE document TYPE int DEFAULT 1;
DEFINE FIELD IF NOT EXISTS deleted_at ON TABLE document TYPE option<datetime>;
DEFINE INDEX IF NOT EXISTS document_owner ON TABLE document FIELDS owner;
DEFINE TABLE IF NOT EXISTS consent SCHEMAFULL
PERMISSIONS
FOR select WHERE user = $auth.id OR $auth.role IN ['operator', 'admin'],
FOR create WHERE user = $auth.id,
FOR update WHERE user = $auth.id,
FOR delete NONE;
DEFINE FIELD IF NOT EXISTS user ON TABLE consent TYPE record<user>;
DEFINE FIELD IF NOT EXISTS scope ON TABLE consent TYPE string;
DEFINE FIELD IF NOT EXISTS granted_at ON TABLE consent TYPE datetime DEFAULT time::now();
DEFINE FIELD IF NOT EXISTS expires_at ON TABLE consent TYPE option<datetime>;
DEFINE FIELD IF NOT EXISTS revoked_at ON TABLE consent TYPE option<datetime>;
DEFINE INDEX IF NOT EXISTS consent_user ON TABLE consent FIELDS user;
DEFINE TABLE IF NOT EXISTS audit_log SCHEMAFULL
PERMISSIONS FOR select WHERE $auth.role = 'admin', FOR create, update, delete NONE;
DEFINE FIELD IF NOT EXISTS actor ON TABLE audit_log TYPE option<record<user>>;
DEFINE FIELD IF NOT EXISTS action ON TABLE audit_log TYPE string;
DEFINE FIELD IF NOT EXISTS target ON TABLE audit_log TYPE string;
DEFINE FIELD IF NOT EXISTS context ON TABLE audit_log TYPE object DEFAULT {};
DEFINE FIELD IF NOT EXISTS at ON TABLE audit_log TYPE datetime DEFAULT time::now();
DEFINE INDEX IF NOT EXISTS audit_log_at ON TABLE audit_log FIELDS at;
@@ -0,0 +1,42 @@
-- TarninaHub taxonomy seed — migration 0002
-- Idempotent. Mirrors the 8 social challenges from the ROPS Map of
-- Social Challenges plus grouped tags and a sample of locations.
INSERT IGNORE INTO social_challenge [
{ id: social_challenge:rodzina, name: 'Rodzina i piecza zastępcza', order: 1 },
{ id: social_challenge:bezdomnosc, name: 'Bezdomność', order: 2 },
{ id: social_challenge:niepelnosprawnosc, name: 'Niepełnosprawność', order: 3 },
{ id: social_challenge:ubostwo, name: 'Ubóstwo', order: 4 },
{ id: social_challenge:integracja_cudzoziemcow, name: 'Integracja cudzoziemców', order: 5 },
{ id: social_challenge:zdrowie, name: 'Zdrowie', order: 6 },
{ id: social_challenge:zdrowie_psychiczne, name: 'Zdrowie psychiczne', order: 7 },
{ id: social_challenge:seniorzy, name: 'Seniorzy', order: 8 }
];
INSERT IGNORE INTO tag [
{ id: tag:seniorzy, name: 'seniorzy', group: 'audience' },
{ id: tag:mlodziez, name: 'młodzież', group: 'audience' },
{ id: tag:rodziny, name: 'rodziny', group: 'audience' },
{ id: tag:osoby_z_niepelnosprawnosciami, name: 'osoby z niepełnosprawnościami', group: 'audience' },
{ id: tag:zdrowie_psychiczne, name: 'zdrowie psychiczne', group: 'topic' },
{ id: tag:samotnosc, name: 'samotność', group: 'topic' },
{ id: tag:transport, name: 'transport', group: 'topic' },
{ id: tag:pomysl, name: 'pomysł', group: 'stage' },
{ id: tag:pilotaz, name: 'pilotaż', group: 'stage' },
{ id: tag:skalowanie, name: 'skalowanie', group: 'stage' },
{ id: tag:usluga, name: 'usługa', group: 'stage' },
{ id: tag:ngo, name: 'NGO', group: 'sector' },
{ id: tag:jst, name: 'JST', group: 'sector' },
{ id: tag:instytucja, name: 'instytucja', group: 'sector' },
{ id: tag:firma, name: 'firma', group: 'sector' },
{ id: tag:wykluczenie_cyfrowe, name: 'wykluczenie cyfrowe', group: 'cross' },
{ id: tag:koordynacja_miedzyinstytucjonalna, name: 'koordynacja międzyinstytucjonalna', group: 'cross' },
{ id: tag:dostep_do_uslug, name: 'dostęp do usług', group: 'cross' }
];
INSERT IGNORE INTO location [
{ id: location:malopolska, name: 'Małopolska', kind: 'wojewodztwo', teryt: '12' },
{ id: location:krakow, name: 'Kraków', kind: 'powiat', teryt: '1261' },
{ id: location:tarnow, name: 'Tarnów', kind: 'powiat', teryt: '1263' },
{ id: location:nowy_sacz, name: 'Nowy Sącz', kind: 'powiat', teryt: '1262' }
];
+15
View File
@@ -0,0 +1,15 @@
-- TarninaHub — migration 0003: additive schema for demo/UI
-- Idempotent, additive. Required before the demo seed (0004).
-- Human-friendly label for synthetic demo profiles (never personal data).
DEFINE FIELD IF NOT EXISTS display_name ON TABLE user TYPE option<string>;
-- Innovations are taggable (matchmaking filters), mirroring `problem.tags`.
DEFINE FIELD IF NOT EXISTS tags ON TABLE innovation TYPE array<record<tag>> DEFAULT [];
-- Free-form payloads must be FLEXIBLE (SCHEMAFULL otherwise rejects nested keys).
DEFINE FIELD OVERWRITE contact ON TABLE organization TYPE option<object> FLEXIBLE;
DEFINE FIELD OVERWRITE content ON TABLE application TYPE object FLEXIBLE DEFAULT {};
DEFINE FIELD OVERWRITE media ON TABLE innovation TYPE array<object> FLEXIBLE DEFAULT [];
DEFINE FIELD OVERWRITE attachments ON TABLE knowledge_item TYPE array<object> FLEXIBLE DEFAULT [];
DEFINE FIELD OVERWRITE payload ON TABLE notification TYPE object FLEXIBLE DEFAULT {};
+117
View File
@@ -0,0 +1,117 @@
-- TarninaHub — migration 0004: synthetic demo seed (step 09)
-- Idempotent (INSERT IGNORE). ALL DATA IS SYNTHETIC — no real personal or
-- sensitive data. Personas come from the public ROPS Map of Social Challenges
-- and are explicitly synthetic. Phone numbers use the +48 000 reserved range.
-- Passwords are generated demo hashes for a throwaway password.
-- ---------------------------------------------------------------------------
-- Organizations
-- ---------------------------------------------------------------------------
INSERT IGNORE INTO organization [
{ id: organization:ngo_dobry_sasiad, name: 'Stowarzyszenie Dobry Sąsiad (demo)', type: 'ngo', location: location:krakow, contact: { email: 'kontakt@example.test' } },
{ id: organization:mops_krakow, name: 'MOPS Kraków (demo)', type: 'jst', location: location:krakow, contact: { email: 'mops@example.test' } },
{ id: organization:instytut_zdrowia, name: 'Instytut Zdrowia Społecznego (demo)', type: 'institution', location: location:krakow, contact: { email: 'instytut@example.test' } },
{ id: organization:firma_care, name: 'CareTech sp. z o.o. (demo)', type: 'company', location: location:tarnow, contact: { email: 'caretech@example.test' } },
{ id: organization:rops_krakow, name: 'ROPS Kraków (demo)', type: 'institution', location: location:krakow, contact: { email: 'rops@example.test' } }
];
-- ---------------------------------------------------------------------------
-- Users (synthetic personas + demo staff roles)
-- ---------------------------------------------------------------------------
INSERT IGNORE INTO user [
{ id: user:ania, display_name: 'Ania (persona)', phone: '+48000000001', email: 'ania@example.test', password: crypto::argon2::generate('Demo1234!'), role: 'user', organization: organization:ngo_dobry_sasiad },
{ id: user:kuba, display_name: 'Kuba (persona)', phone: '+48000000002', email: 'kuba@example.test', password: crypto::argon2::generate('Demo1234!'), role: 'user' },
{ id: user:krystian, display_name: 'Krystian (persona)', phone: '+48000000003', email: 'krystian@example.test', password: crypto::argon2::generate('Demo1234!'), role: 'user' },
{ id: user:tomek, display_name: 'Tomek (persona)', phone: '+48000000004', email: 'tomek@example.test', password: crypto::argon2::generate('Demo1234!'), role: 'user' },
{ id: user:swietlana, display_name: 'Swietłana (persona)', phone: '+48000000005', email: 'swietlana@example.test', password: crypto::argon2::generate('Demo1234!'), role: 'user' },
{ id: user:stanislaw, display_name: 'Stanisław (persona)', phone: '+48000000006', email: 'stanislaw@example.test', password: crypto::argon2::generate('Demo1234!'), role: 'user' },
{ id: user:mateusz, display_name: 'Mateusz (persona)', phone: '+48000000007', email: 'mateusz@example.test', password: crypto::argon2::generate('Demo1234!'), role: 'user' },
{ id: user:karina, display_name: 'Karina (persona)', phone: '+48000000008', email: 'karina@example.test', password: crypto::argon2::generate('Demo1234!'), role: 'user' },
{ id: user:janina, display_name: 'Janina (persona)', phone: '+48000000009', email: 'janina@example.test', password: crypto::argon2::generate('Demo1234!'), role: 'user' },
{ id: user:jst_krakow, display_name: 'Pracownik JST (demo)', phone: '+48000000010', email: 'jst@example.test', password: crypto::argon2::generate('Demo1234!'), role: 'jst', organization: organization:mops_krakow },
{ id: user:rops_operator, display_name: 'Koordynator ROPS (demo)', phone: '+48000000011', email: 'operator@example.test', password: crypto::argon2::generate('Demo1234!'), role: 'operator', organization: organization:rops_krakow },
{ id: user:rops_admin, display_name: 'Administrator ROPS (demo)', phone: '+48000000012', email: 'admin@example.test', password: crypto::argon2::generate('Demo1234!'), role: 'admin', organization: organization:rops_krakow },
{ id: user:ekspert_zdrowie, display_name: 'Ekspert zdrowia (demo)', phone: '+48000000013', email: 'expert@example.test', password: crypto::argon2::generate('Demo1234!'), role: 'manager', organization: organization:instytut_zdrowia }
];
-- ---------------------------------------------------------------------------
-- Problems + submissions (matchmaking input, one per challenge)
-- ---------------------------------------------------------------------------
INSERT IGNORE INTO problem [
{ id: problem:rodzina_rodzenstwo, title: 'Rodzeństwo rozdzielane przy pieczy zastępczej', description: 'Brak miejsc, które pozwalają utrzymać rodzeństwo razem w pieczy rodzinnej.', challenge: social_challenge:rodzina, location: location:krakow, author: user:ania, visibility: 'public', tags: [tag:rodziny, tag:koordynacja_miedzyinstytucjonalna] },
{ id: problem:mloda_bezdomnosc, title: 'Młoda bezdomność bez wsparcia po placówce', description: 'Osoby 18–25 lat po opuszczeniu placówki nie mają oferty wsparcia.', challenge: social_challenge:bezdomnosc, location: location:krakow, author: user:kuba, visibility: 'public', tags: [tag:mlodziez, tag:dostep_do_uslug] },
{ id: problem:praca_on, title: 'Niska aktywność zawodowa osób z niepełnosprawnościami', description: 'Trudność w znalezieniu i utrzymaniu pracy mimo kwalifikacji.', challenge: social_challenge:niepelnosprawnosc, location: location:tarnow, author: user:krystian, visibility: 'public', tags: [tag:osoby_z_niepelnosprawnosciami, tag:usluga] },
{ id: problem:ubostwo_energetyczne, title: 'Ubóstwo energetyczne i żywnościowe seniorów', description: 'Starsze osoby ograniczają ogrzewanie i posiłki z powodu kosztów.', challenge: social_challenge:ubostwo, location: location:nowy_sacz, author: user:tomek, visibility: 'public', tags: [tag:seniorzy, tag:dostep_do_uslug] },
{ id: problem:integracja_jezyk, title: 'Bariera językowa w dostępie do usług', description: 'Cudzoziemcy nie znają procedur i potrzebują asysty kulturowej.', challenge: social_challenge:integracja_cudzoziemcow, location: location:krakow, author: user:swietlana, visibility: 'public', tags: [tag:dostep_do_uslug, tag:wykluczenie_cyfrowe] },
{ id: problem:samotnosc_seniorow, title: 'Samotność i izolacja seniorów', description: 'Mieszkańcy 70+ nie mają kontaktu z otoczeniem i dostępu do informacji.', challenge: social_challenge:zdrowie, location: location:krakow, author: user:stanislaw, visibility: 'public', tags: [tag:seniorzy, tag:samotnosc] },
{ id: problem:zdrowie_psychiczne_mlodziez, title: 'Kryzys zdrowia psychicznego wśród młodzieży', description: 'Wzrost zaburzeń i trudny dostęp do wsparcia środowiskowego.', challenge: social_challenge:zdrowie_psychiczne, location: location:krakow, author: user:mateusz, visibility: 'public', tags: [tag:mlodziez, tag:zdrowie_psychiczne] },
{ id: problem:powrot_na_rynek, title: 'Powrót na rynek pracy po kryzysie psychicznym', description: 'Stigma i brak elastycznych form zatrudnienia utrudniają powrót.', challenge: social_challenge:zdrowie_psychiczne, location: location:tarnow, author: user:karina, visibility: 'public', tags: [tag:zdrowie_psychiczne, tag:usluga] }
];
INSERT IGNORE INTO submission [
{ id: submission:rodzina_rodzenstwo, problem: problem:rodzina_rodzenstwo, submitted_by: user:ania, status: 'matched', keywords: ['rodzeństwo', 'piecza', 'rodzina'] },
{ id: submission:mloda_bezdomnosc, problem: problem:mloda_bezdomnosc, submitted_by: user:kuba, status: 'triaged', keywords: ['bezdomność', 'młodzież', 'aktywizacja'] },
{ id: submission:praca_on, problem: problem:praca_on, submitted_by: user:krystian, status: 'matched', keywords: ['praca', 'niepełnosprawność', 'edukacja'] },
{ id: submission:ubostwo_energetyczne, problem: problem:ubostwo_energetyczne, submitted_by: user:tomek, status: 'new', keywords: ['ubóstwo', 'seniorzy', 'energia'] },
{ id: submission:integracja_jezyk, problem: problem:integracja_jezyk, submitted_by: user:swietlana, status: 'matched', keywords: ['cudzoziemcy', 'integracja', 'język'] },
{ id: submission:samotnosc_seniorow, problem: problem:samotnosc_seniorow, submitted_by: user:stanislaw, status: 'matched', keywords: ['samotność', 'seniorzy', 'cyfryzacja'] },
{ id: submission:zdrowie_psychiczne_mlodziez, problem: problem:zdrowie_psychiczne_mlodziez, submitted_by: user:mateusz, status: 'matched', keywords: ['zdrowie psychiczne', 'młodzież', 'profilaktyka'] },
{ id: submission:powrot_na_rynek, problem: problem:powrot_na_rynek, submitted_by: user:karina, status: 'in_progress', keywords: ['zdrowie psychiczne', 'praca', 'reintegracja'] }
];
-- ---------------------------------------------------------------------------
-- Innovations (catalogue, one or more per challenge)
-- ---------------------------------------------------------------------------
INSERT IGNORE INTO innovation [
{ id: innovation:rodziny_grupy, title: 'Grupy wsparcia dla rodzin zastępczych', summary: 'Sieć sąsiedzka utrzymująca rodzeństwo razem.', description: 'Lokalne grupy rodzin zastępczych dzielą się opieką i wsparciem, ograniczając rozdzielanie rodzeństwa.', challenge: social_challenge:rodzina, stage: 'pilot', published: true, tags: [tag:rodziny, tag:koordynacja_miedzyinstytucjonalna] },
{ id: innovation:hostel_mlodzi, title: 'Hostel aktywizacyjny dla młodych dorosłych', summary: 'Mieszkanie z programem usamodzielniania.', description: 'Połączenie schronienia i treningu kompetencji społecznych dla osób 18–25 lat.', challenge: social_challenge:bezdomnosc, stage: 'scaling', published: true, tags: [tag:mlodziez, tag:dostep_do_uslug] },
{ id: innovation:praca_wspierana, title: 'Praca wspierana z doradcą zawodowym', summary: 'Model zatrudnienia wspieranego w tandemie z trenerem pracy.', description: 'Trener pracy wspiera zatrudnienie osoby z niepełnosprawnością u lokalnego pracodawcy.', challenge: social_challenge:niepelnosprawnosc, stage: 'service', published: true, tags: [tag:osoby_z_niepelnosprawnosciami, tag:usluga] },
{ id: innovation:spizarnia, title: 'Sąsiedzka spizarnia i wymiana energii', summary: 'Zbiórki żywności i wsparcie kosztów ogrzewania.', description: 'Lokalne spizarnie społeczne i sąsiedzka pomoc w kosztach energii dla seniorów.', challenge: social_challenge:ubostwo, stage: 'pilot', published: true, tags: [tag:seniorzy, tag:dostep_do_uslug] },
{ id: innovation:asysta_cudzoziemcy, title: 'Asysta kulturowa dla cudzoziemców', summary: 'Tłumacz-asystent prowadzący przez procedury.', description: 'Asystent kulturowy pomaga w urzędzie, szkole i służbie zdrowia.', challenge: social_challenge:integracja_cudzoziemcow, stage: 'service', published: true, tags: [tag:dostep_do_uslug, tag:wykluczenie_cyfrowe] },
{ id: innovation:telefon_zaufania_senior, title: 'Telefon i wizyta sąsiedzka dla seniorów', summary: 'Regularny kontakt i pomoc w cyfryzacji.', description: 'Wolontariusze dzwonią i odwiedzają seniorów, ucząc korzystania z usług online.', challenge: social_challenge:zdrowie, stage: 'scaling', published: true, tags: [tag:seniorzy, tag:samotnosc] },
{ id: innovation:mlodzi_glowy_szkola, title: 'Program wsparcia psychicznego w szkołach', summary: 'Warsztaty i grupy rówieśnicze.', description: 'Szkolne grupy wsparcia i warsztaty profilaktyczne dla młodzieży.', challenge: social_challenge:zdrowie_psychiczne, stage: 'pilot', published: true, tags: [tag:mlodziez, tag:zdrowie_psychiczne] },
{ id: innovation:zatrudnienie_elastyczne, title: 'Elastyczne zatrudnienie po kryzysie', summary: 'Stopniowy powrót na rynek pracy.', description: 'Elastyczne formy pracy i wsparcie mentora dla osób po kryzysie psychicznym.', challenge: social_challenge:zdrowie_psychiczne, stage: 'service', published: true, tags: [tag:zdrowie_psychiczne, tag:usluga] }
];
-- ---------------------------------------------------------------------------
-- Knowledge base (metadata + public links only, no copied content)
-- ---------------------------------------------------------------------------
INSERT IGNORE INTO knowledge_item [
{ id: knowledge_item:raport_rodzina, title: 'Piecza zastępcza w Polsce — raport GUS', kind: 'report', challenge: social_challenge:rodzina, source_url: 'https://stat.gov.pl/obszary-tematyczne/dzieci-i-rodzina/dzieci/piecza-zastepcza-w-2023-roku,1,8.html', published: true },
{ id: knowledge_item:raport_bezdomnosc, title: 'Młoda bezdomność — raport Fundacji po DRUGIE', kind: 'report', challenge: social_challenge:bezdomnosc, source_url: 'https://podrugie.pl/wp-content/uploads/2023/04/RAPORT-DODAJ-MNIE.pdf', published: true },
{ id: knowledge_item:raport_on, title: 'Badanie potrzeb osób niepełnosprawnych — PFRON 2024', kind: 'report', challenge: social_challenge:niepelnosprawnosc, source_url: 'https://www.pfron.org.pl/instytucje/badania-i-analizy-naukowe/raport-koncowy-z-badania-potrzeb-osob-niepelnosprawnych/', published: true },
{ id: knowledge_item:raport_ubostwo, title: 'Poverty Watch — EAPN Polska', kind: 'report', challenge: social_challenge:ubostwo, source_url: 'https://www.eapn.org.pl/eapn/uploads/2023/10/poverty_watch_23_v12_10_v2_ost.pdf', published: true },
{ id: knowledge_item:mapa_wyzwan, title: 'Mapa Wyzwań Społecznych — ROPS Kraków', kind: 'map', challenge: social_challenge:integracja_cudzoziemcow, source_url: 'https://rops.krakow.pl/mpliki/IS/IWS_20/za._nr_2._Mapa_Wyzwa_Spoecznych.pdf', published: true },
{ id: knowledge_item:raport_samotnosc, title: 'Raport o samotności — Szlachetna Paczka', kind: 'report', challenge: social_challenge:zdrowie, source_url: 'https://www.szlachetnapaczka.pl/wp-content/uploads/2021/03/raport_o_samotnosci_2021.pdf', published: true },
{ id: knowledge_item:raport_mlode_glowy, title: 'Młode Głowy — raport o zdrowiu psychicznym', kind: 'report', challenge: social_challenge:zdrowie_psychiczne, source_url: 'https://mlodeglowy.pl/wp-content/uploads/2023/04/MLODE-GLOWY.-Otwarcie-o-zdrowiu-psychicznym_-Raport-final.pdf', published: true },
{ id: knowledge_item:raport_seniorzy, title: 'Sytuacja osób starszych w Polsce — GUS 2022', kind: 'report', challenge: social_challenge:seniorzy, source_url: 'https://stat.gov.pl/obszary-tematyczne/osoby-starsze/osoby-starsze/sytuacja-osob-starszych-w-polscie-w-2022-roku,2,5.html', published: true }
];
-- ---------------------------------------------------------------------------
-- Grant calls + ideas + test feedback
-- ---------------------------------------------------------------------------
INSERT IGNORE INTO grant_call [
{ id: grant_call:innowacje_2026, name: 'Innowacje społeczne 2026 (demo)', opens_at: d'2026-09-01T00:00:00Z', closes_at: d'2026-12-31T23:59:59Z', status: 'open', external_id: 'DEMO/5.1/2026-01' },
{ id: grant_call:seniorzy_2026, name: 'Włączenie seniorów 2026 (demo)', opens_at: d'2026-10-01T00:00:00Z', closes_at: d'2026-11-30T23:59:59Z', status: 'open', external_id: 'DEMO/5.1/2026-02' }
];
INSERT IGNORE INTO idea [
{ id: idea:klub_sasiedzki, title: 'Klub sąsiedzki dla seniorów', description: 'Miejsce spotkań i nauki cyfrowej dla osób starszych.', author: user:janina, status: 'submitted' },
{ id: idea:rower_terapeutyczny, title: 'Rower cargo dla wsparcia psychicznego', description: 'Wspólne wyjazdy jako forma terapii młodzieży.', author: user:mateusz, status: 'in_review' },
{ id: idea:doradca_online, title: 'Doradca online dla osób z niepełnosprawnościami', description: 'Zdalne wsparcie w szukaniu pracy.', author: user:krystian, status: 'draft' }
];
INSERT IGNORE INTO application [
{ id: application:klub_sasiedzki, grant_call: grant_call:seniorzy_2026, idea: idea:klub_sasiedzki, content: { budget: 'syntetyczny', scope: 'klub sąsiedzki' }, status: 'submitted' }
];
INSERT IGNORE INTO test_feedback [
{ id: test_feedback:grupy_rodzinne_1, innovation: innovation:rodziny_grupy, author: user:ania, rating: 5, comment: 'Model dobrze łączy rodziny.', improvements: 'Potrzebna koordynacja powiatowa.' },
{ id: test_feedback:mlodzi_glowy_1, innovation: innovation:mlodzi_glowy_szkola, author: user:karina, rating: 4, comment: 'Warsztaty potrzebne i trafne.', improvements: 'Zwiększyć liczbę grup rówieśniczych.' }
];
+42
View File
@@ -0,0 +1,42 @@
# Migracje bazy danych (SurrealDB)
Migracje w SurrealQL, wersjonowane w git. Backend (plik/RocksDB) jest źródłem prawdy; frontend (WASM) stosuje ten sam podzbiór definicji przy starcie.
## Pliki
| Plik | Zawartość |
|---|---|
| `0001_schema.surql` | tabele, pola, indeksy (w tym HNSW + pełnotekstowy), `ACCESS`, `PERMISSIONS`, `ASSERT` |
| `0002_taxonomy_seed.surql` | 8 obszarów (`social_challenge`), tagi, próbka lokalizacji |
| `0003_demo_schema.surql` | dodatki schematu pod demo: `user.display_name`, `innovation.tags`, pola `FLEXIBLE` |
| `0004_demo_seed.surql` | dane syntetyczne (persony, problemy, innowacje, wiedza, nabory) |
## Zasady
- Numeracja `NNNN_nazwa.surql`, stosowana w kolejności rosnącej.
- Migracje **idempotentne** (`DEFINE ... OVERWRITE`, `INSERT IGNORE`), addytywne.
- Zmiany schematu wyłącznie przez nowy plik migracji — bez ręcznych zmian na środowisku.
- Brak danych osobowych/wrażliwych w seedach (krok 09).
## Aplikowanie
SurrealDB CLI 3.x. `surreal sql` nie ma już flagi `--file` — przekaż plik przez stdin z `--multi` (średniki rozdzielają instrukcje):
```pwsh
Get-Content -Raw backend/migrations/0001_schema.surql | `
surreal sql -e http://127.0.0.1:18010 -u root -p root --ns hubmi --db hubmi --hide-welcome --multi
Get-Content -Raw backend/migrations/0002_taxonomy_seed.surql | `
surreal sql -e http://127.0.0.1:18010 -u root -p root --ns hubmi --db hubmi --hide-welcome --multi
```
Alternatywnie `surreal import` — wymaga `OPTION IMPORT;` jako pierwszej instrukcji pliku (wyłącza przetwarzanie pól).
Embedded RocksDB: uruchom `pwsh ./scripts/surrealdb-start.ps1` (tryb zdalny) albo zastosuj migracje przez backend przy starcie (krok 10).
## Weryfikacja
Zwalidowano na **SurrealDB 3.2.4** (instancja in-memory): schemat i seed aplikują się bez błędów, ponowne uruchomienie jest idempotentne (22 tabele, 8 obszarów, 18 tagów, 4 lokalizacje), a CRUD na encjach (`create`/`update`/`select`/`delete`) działa.
## Frontend (WASM)
Frontend stosuje podzbiór `0001_schema.surql` do `indxdb://tarninahub` (bez serwerowych `PERMISSIONS` i `ACCESS`). Reguły synchronizacji: patrz `10_ZalozeniaProjektowe/todo08/BAZA_DANYCH.md` §6.