Sifa professional network API (Fastify, AT Protocol, Jetstream) sifa.id/

feat(db): add core schema -- profiles, positions, education, skills

Drizzle ORM schema tables mapping to id.sifa.profile.* lexicons:
- profiles: professional identity (did PK, handle, headline, location, etc.)
- positions: work history (composite PK did+rkey, FK to profiles with cascade)
- education: academic history (composite PK did+rkey, FK to profiles with cascade)
- skills: professional skills (composite PK did+rkey, FK to profiles with cascade)

Includes initial migration (0000) and schema tests.

+637 -1
+67
drizzle/0000_striped_skaar.sql
··· 1 + CREATE TABLE "education" ( 2 + "did" text NOT NULL, 3 + "rkey" text NOT NULL, 4 + "institution" text NOT NULL, 5 + "institution_did" text, 6 + "degree" text, 7 + "field_of_study" text, 8 + "description" text, 9 + "start_date" text, 10 + "end_date" text, 11 + "created_at" timestamp with time zone NOT NULL, 12 + "indexed_at" timestamp with time zone DEFAULT now() NOT NULL, 13 + CONSTRAINT "education_did_rkey_pk" PRIMARY KEY("did","rkey") 14 + ); 15 + --> statement-breakpoint 16 + CREATE TABLE "positions" ( 17 + "did" text NOT NULL, 18 + "rkey" text NOT NULL, 19 + "company_name" text NOT NULL, 20 + "company_did" text, 21 + "title" text NOT NULL, 22 + "description" text, 23 + "employment_type" text, 24 + "workplace_type" text, 25 + "location_country" text, 26 + "location_region" text, 27 + "location_city" text, 28 + "start_date" text NOT NULL, 29 + "end_date" text, 30 + "current" boolean DEFAULT false NOT NULL, 31 + "created_at" timestamp with time zone NOT NULL, 32 + "indexed_at" timestamp with time zone DEFAULT now() NOT NULL, 33 + CONSTRAINT "positions_did_rkey_pk" PRIMARY KEY("did","rkey") 34 + ); 35 + --> statement-breakpoint 36 + CREATE TABLE "profiles" ( 37 + "did" text PRIMARY KEY NOT NULL, 38 + "handle" text NOT NULL, 39 + "headline" text, 40 + "about" text, 41 + "industry" text, 42 + "location_country" text, 43 + "location_region" text, 44 + "location_city" text, 45 + "website" text, 46 + "open_to" text[], 47 + "preferred_workplace" text[], 48 + "langs" text[], 49 + "created_at" timestamp with time zone NOT NULL, 50 + "indexed_at" timestamp with time zone DEFAULT now() NOT NULL, 51 + "updated_at" timestamp with time zone DEFAULT now() NOT NULL 52 + ); 53 + --> statement-breakpoint 54 + CREATE TABLE "skills" ( 55 + "did" text NOT NULL, 56 + "rkey" text NOT NULL, 57 + "skill_name" text NOT NULL, 58 + "category" text, 59 + "created_at" timestamp with time zone NOT NULL, 60 + "indexed_at" timestamp with time zone DEFAULT now() NOT NULL, 61 + CONSTRAINT "skills_did_rkey_pk" PRIMARY KEY("did","rkey") 62 + ); 63 + --> statement-breakpoint 64 + ALTER TABLE "education" ADD CONSTRAINT "education_did_profiles_did_fk" FOREIGN KEY ("did") REFERENCES "public"."profiles"("did") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 65 + ALTER TABLE "positions" ADD CONSTRAINT "positions_did_profiles_did_fk" FOREIGN KEY ("did") REFERENCES "public"."profiles"("did") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 66 + ALTER TABLE "skills" ADD CONSTRAINT "skills_did_profiles_did_fk" FOREIGN KEY ("did") REFERENCES "public"."profiles"("did") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 67 + CREATE INDEX "idx_profiles_handle" ON "profiles" USING btree ("handle");
+447
drizzle/meta/0000_snapshot.json
··· 1 + { 2 + "id": "17adc10e-4734-4b3f-aa2d-0a9663b11159", 3 + "prevId": "00000000-0000-0000-0000-000000000000", 4 + "version": "7", 5 + "dialect": "postgresql", 6 + "tables": { 7 + "public.education": { 8 + "name": "education", 9 + "schema": "", 10 + "columns": { 11 + "did": { 12 + "name": "did", 13 + "type": "text", 14 + "primaryKey": false, 15 + "notNull": true 16 + }, 17 + "rkey": { 18 + "name": "rkey", 19 + "type": "text", 20 + "primaryKey": false, 21 + "notNull": true 22 + }, 23 + "institution": { 24 + "name": "institution", 25 + "type": "text", 26 + "primaryKey": false, 27 + "notNull": true 28 + }, 29 + "institution_did": { 30 + "name": "institution_did", 31 + "type": "text", 32 + "primaryKey": false, 33 + "notNull": false 34 + }, 35 + "degree": { 36 + "name": "degree", 37 + "type": "text", 38 + "primaryKey": false, 39 + "notNull": false 40 + }, 41 + "field_of_study": { 42 + "name": "field_of_study", 43 + "type": "text", 44 + "primaryKey": false, 45 + "notNull": false 46 + }, 47 + "description": { 48 + "name": "description", 49 + "type": "text", 50 + "primaryKey": false, 51 + "notNull": false 52 + }, 53 + "start_date": { 54 + "name": "start_date", 55 + "type": "text", 56 + "primaryKey": false, 57 + "notNull": false 58 + }, 59 + "end_date": { 60 + "name": "end_date", 61 + "type": "text", 62 + "primaryKey": false, 63 + "notNull": false 64 + }, 65 + "created_at": { 66 + "name": "created_at", 67 + "type": "timestamp with time zone", 68 + "primaryKey": false, 69 + "notNull": true 70 + }, 71 + "indexed_at": { 72 + "name": "indexed_at", 73 + "type": "timestamp with time zone", 74 + "primaryKey": false, 75 + "notNull": true, 76 + "default": "now()" 77 + } 78 + }, 79 + "indexes": {}, 80 + "foreignKeys": { 81 + "education_did_profiles_did_fk": { 82 + "name": "education_did_profiles_did_fk", 83 + "tableFrom": "education", 84 + "tableTo": "profiles", 85 + "columnsFrom": [ 86 + "did" 87 + ], 88 + "columnsTo": [ 89 + "did" 90 + ], 91 + "onDelete": "cascade", 92 + "onUpdate": "no action" 93 + } 94 + }, 95 + "compositePrimaryKeys": { 96 + "education_did_rkey_pk": { 97 + "name": "education_did_rkey_pk", 98 + "columns": [ 99 + "did", 100 + "rkey" 101 + ] 102 + } 103 + }, 104 + "uniqueConstraints": {}, 105 + "policies": {}, 106 + "checkConstraints": {}, 107 + "isRLSEnabled": false 108 + }, 109 + "public.positions": { 110 + "name": "positions", 111 + "schema": "", 112 + "columns": { 113 + "did": { 114 + "name": "did", 115 + "type": "text", 116 + "primaryKey": false, 117 + "notNull": true 118 + }, 119 + "rkey": { 120 + "name": "rkey", 121 + "type": "text", 122 + "primaryKey": false, 123 + "notNull": true 124 + }, 125 + "company_name": { 126 + "name": "company_name", 127 + "type": "text", 128 + "primaryKey": false, 129 + "notNull": true 130 + }, 131 + "company_did": { 132 + "name": "company_did", 133 + "type": "text", 134 + "primaryKey": false, 135 + "notNull": false 136 + }, 137 + "title": { 138 + "name": "title", 139 + "type": "text", 140 + "primaryKey": false, 141 + "notNull": true 142 + }, 143 + "description": { 144 + "name": "description", 145 + "type": "text", 146 + "primaryKey": false, 147 + "notNull": false 148 + }, 149 + "employment_type": { 150 + "name": "employment_type", 151 + "type": "text", 152 + "primaryKey": false, 153 + "notNull": false 154 + }, 155 + "workplace_type": { 156 + "name": "workplace_type", 157 + "type": "text", 158 + "primaryKey": false, 159 + "notNull": false 160 + }, 161 + "location_country": { 162 + "name": "location_country", 163 + "type": "text", 164 + "primaryKey": false, 165 + "notNull": false 166 + }, 167 + "location_region": { 168 + "name": "location_region", 169 + "type": "text", 170 + "primaryKey": false, 171 + "notNull": false 172 + }, 173 + "location_city": { 174 + "name": "location_city", 175 + "type": "text", 176 + "primaryKey": false, 177 + "notNull": false 178 + }, 179 + "start_date": { 180 + "name": "start_date", 181 + "type": "text", 182 + "primaryKey": false, 183 + "notNull": true 184 + }, 185 + "end_date": { 186 + "name": "end_date", 187 + "type": "text", 188 + "primaryKey": false, 189 + "notNull": false 190 + }, 191 + "current": { 192 + "name": "current", 193 + "type": "boolean", 194 + "primaryKey": false, 195 + "notNull": true, 196 + "default": false 197 + }, 198 + "created_at": { 199 + "name": "created_at", 200 + "type": "timestamp with time zone", 201 + "primaryKey": false, 202 + "notNull": true 203 + }, 204 + "indexed_at": { 205 + "name": "indexed_at", 206 + "type": "timestamp with time zone", 207 + "primaryKey": false, 208 + "notNull": true, 209 + "default": "now()" 210 + } 211 + }, 212 + "indexes": {}, 213 + "foreignKeys": { 214 + "positions_did_profiles_did_fk": { 215 + "name": "positions_did_profiles_did_fk", 216 + "tableFrom": "positions", 217 + "tableTo": "profiles", 218 + "columnsFrom": [ 219 + "did" 220 + ], 221 + "columnsTo": [ 222 + "did" 223 + ], 224 + "onDelete": "cascade", 225 + "onUpdate": "no action" 226 + } 227 + }, 228 + "compositePrimaryKeys": { 229 + "positions_did_rkey_pk": { 230 + "name": "positions_did_rkey_pk", 231 + "columns": [ 232 + "did", 233 + "rkey" 234 + ] 235 + } 236 + }, 237 + "uniqueConstraints": {}, 238 + "policies": {}, 239 + "checkConstraints": {}, 240 + "isRLSEnabled": false 241 + }, 242 + "public.profiles": { 243 + "name": "profiles", 244 + "schema": "", 245 + "columns": { 246 + "did": { 247 + "name": "did", 248 + "type": "text", 249 + "primaryKey": true, 250 + "notNull": true 251 + }, 252 + "handle": { 253 + "name": "handle", 254 + "type": "text", 255 + "primaryKey": false, 256 + "notNull": true 257 + }, 258 + "headline": { 259 + "name": "headline", 260 + "type": "text", 261 + "primaryKey": false, 262 + "notNull": false 263 + }, 264 + "about": { 265 + "name": "about", 266 + "type": "text", 267 + "primaryKey": false, 268 + "notNull": false 269 + }, 270 + "industry": { 271 + "name": "industry", 272 + "type": "text", 273 + "primaryKey": false, 274 + "notNull": false 275 + }, 276 + "location_country": { 277 + "name": "location_country", 278 + "type": "text", 279 + "primaryKey": false, 280 + "notNull": false 281 + }, 282 + "location_region": { 283 + "name": "location_region", 284 + "type": "text", 285 + "primaryKey": false, 286 + "notNull": false 287 + }, 288 + "location_city": { 289 + "name": "location_city", 290 + "type": "text", 291 + "primaryKey": false, 292 + "notNull": false 293 + }, 294 + "website": { 295 + "name": "website", 296 + "type": "text", 297 + "primaryKey": false, 298 + "notNull": false 299 + }, 300 + "open_to": { 301 + "name": "open_to", 302 + "type": "text[]", 303 + "primaryKey": false, 304 + "notNull": false 305 + }, 306 + "preferred_workplace": { 307 + "name": "preferred_workplace", 308 + "type": "text[]", 309 + "primaryKey": false, 310 + "notNull": false 311 + }, 312 + "langs": { 313 + "name": "langs", 314 + "type": "text[]", 315 + "primaryKey": false, 316 + "notNull": false 317 + }, 318 + "created_at": { 319 + "name": "created_at", 320 + "type": "timestamp with time zone", 321 + "primaryKey": false, 322 + "notNull": true 323 + }, 324 + "indexed_at": { 325 + "name": "indexed_at", 326 + "type": "timestamp with time zone", 327 + "primaryKey": false, 328 + "notNull": true, 329 + "default": "now()" 330 + }, 331 + "updated_at": { 332 + "name": "updated_at", 333 + "type": "timestamp with time zone", 334 + "primaryKey": false, 335 + "notNull": true, 336 + "default": "now()" 337 + } 338 + }, 339 + "indexes": { 340 + "idx_profiles_handle": { 341 + "name": "idx_profiles_handle", 342 + "columns": [ 343 + { 344 + "expression": "handle", 345 + "isExpression": false, 346 + "asc": true, 347 + "nulls": "last" 348 + } 349 + ], 350 + "isUnique": false, 351 + "concurrently": false, 352 + "method": "btree", 353 + "with": {} 354 + } 355 + }, 356 + "foreignKeys": {}, 357 + "compositePrimaryKeys": {}, 358 + "uniqueConstraints": {}, 359 + "policies": {}, 360 + "checkConstraints": {}, 361 + "isRLSEnabled": false 362 + }, 363 + "public.skills": { 364 + "name": "skills", 365 + "schema": "", 366 + "columns": { 367 + "did": { 368 + "name": "did", 369 + "type": "text", 370 + "primaryKey": false, 371 + "notNull": true 372 + }, 373 + "rkey": { 374 + "name": "rkey", 375 + "type": "text", 376 + "primaryKey": false, 377 + "notNull": true 378 + }, 379 + "skill_name": { 380 + "name": "skill_name", 381 + "type": "text", 382 + "primaryKey": false, 383 + "notNull": true 384 + }, 385 + "category": { 386 + "name": "category", 387 + "type": "text", 388 + "primaryKey": false, 389 + "notNull": false 390 + }, 391 + "created_at": { 392 + "name": "created_at", 393 + "type": "timestamp with time zone", 394 + "primaryKey": false, 395 + "notNull": true 396 + }, 397 + "indexed_at": { 398 + "name": "indexed_at", 399 + "type": "timestamp with time zone", 400 + "primaryKey": false, 401 + "notNull": true, 402 + "default": "now()" 403 + } 404 + }, 405 + "indexes": {}, 406 + "foreignKeys": { 407 + "skills_did_profiles_did_fk": { 408 + "name": "skills_did_profiles_did_fk", 409 + "tableFrom": "skills", 410 + "tableTo": "profiles", 411 + "columnsFrom": [ 412 + "did" 413 + ], 414 + "columnsTo": [ 415 + "did" 416 + ], 417 + "onDelete": "cascade", 418 + "onUpdate": "no action" 419 + } 420 + }, 421 + "compositePrimaryKeys": { 422 + "skills_did_rkey_pk": { 423 + "name": "skills_did_rkey_pk", 424 + "columns": [ 425 + "did", 426 + "rkey" 427 + ] 428 + } 429 + }, 430 + "uniqueConstraints": {}, 431 + "policies": {}, 432 + "checkConstraints": {}, 433 + "isRLSEnabled": false 434 + } 435 + }, 436 + "enums": {}, 437 + "schemas": {}, 438 + "sequences": {}, 439 + "roles": {}, 440 + "policies": {}, 441 + "views": {}, 442 + "_meta": { 443 + "columns": {}, 444 + "schemas": {}, 445 + "tables": {} 446 + } 447 + }
+13
drizzle/meta/_journal.json
··· 1 + { 2 + "version": "7", 3 + "dialect": "postgresql", 4 + "entries": [ 5 + { 6 + "idx": 0, 7 + "version": "7", 8 + "when": 1772980747851, 9 + "tag": "0000_striped_skaar", 10 + "breakpoints": true 11 + } 12 + ] 13 + }
+18
src/db/schema/education.ts
··· 1 + import { pgTable, text, timestamp, primaryKey } from 'drizzle-orm/pg-core'; 2 + import { profiles } from './profiles.js'; 3 + 4 + export const education = pgTable('education', { 5 + did: text('did').notNull().references(() => profiles.did, { onDelete: 'cascade' }), 6 + rkey: text('rkey').notNull(), 7 + institution: text('institution').notNull(), 8 + institutionDid: text('institution_did'), 9 + degree: text('degree'), 10 + fieldOfStudy: text('field_of_study'), 11 + description: text('description'), 12 + startDate: text('start_date'), 13 + endDate: text('end_date'), 14 + createdAt: timestamp('created_at', { withTimezone: true }).notNull(), 15 + indexedAt: timestamp('indexed_at', { withTimezone: true }).notNull().defaultNow(), 16 + }, (table) => [ 17 + primaryKey({ columns: [table.did, table.rkey] }), 18 + ]);
+4 -1
src/db/schema/index.ts
··· 1 - // Schema tables will be added in subsequent tasks 1 + export { profiles } from './profiles.js'; 2 + export { positions } from './positions.js'; 3 + export { education } from './education.js'; 4 + export { skills } from './skills.js';
+23
src/db/schema/positions.ts
··· 1 + import { pgTable, text, timestamp, boolean, primaryKey } from 'drizzle-orm/pg-core'; 2 + import { profiles } from './profiles.js'; 3 + 4 + export const positions = pgTable('positions', { 5 + did: text('did').notNull().references(() => profiles.did, { onDelete: 'cascade' }), 6 + rkey: text('rkey').notNull(), 7 + companyName: text('company_name').notNull(), 8 + companyDid: text('company_did'), 9 + title: text('title').notNull(), 10 + description: text('description'), 11 + employmentType: text('employment_type'), 12 + workplaceType: text('workplace_type'), 13 + locationCountry: text('location_country'), 14 + locationRegion: text('location_region'), 15 + locationCity: text('location_city'), 16 + startDate: text('start_date').notNull(), 17 + endDate: text('end_date'), 18 + current: boolean('current').notNull().default(false), 19 + createdAt: timestamp('created_at', { withTimezone: true }).notNull(), 20 + indexedAt: timestamp('indexed_at', { withTimezone: true }).notNull().defaultNow(), 21 + }, (table) => [ 22 + primaryKey({ columns: [table.did, table.rkey] }), 23 + ]);
+21
src/db/schema/profiles.ts
··· 1 + import { pgTable, text, timestamp, index } from 'drizzle-orm/pg-core'; 2 + 3 + export const profiles = pgTable('profiles', { 4 + did: text('did').primaryKey(), 5 + handle: text('handle').notNull(), 6 + headline: text('headline'), 7 + about: text('about'), 8 + industry: text('industry'), 9 + locationCountry: text('location_country'), 10 + locationRegion: text('location_region'), 11 + locationCity: text('location_city'), 12 + website: text('website'), 13 + openTo: text('open_to').array(), 14 + preferredWorkplace: text('preferred_workplace').array(), 15 + langs: text('langs').array(), 16 + createdAt: timestamp('created_at', { withTimezone: true }).notNull(), 17 + indexedAt: timestamp('indexed_at', { withTimezone: true }).notNull().defaultNow(), 18 + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), 19 + }, (table) => [ 20 + index('idx_profiles_handle').on(table.handle), 21 + ]);
+13
src/db/schema/skills.ts
··· 1 + import { pgTable, text, timestamp, primaryKey } from 'drizzle-orm/pg-core'; 2 + import { profiles } from './profiles.js'; 3 + 4 + export const skills = pgTable('skills', { 5 + did: text('did').notNull().references(() => profiles.did, { onDelete: 'cascade' }), 6 + rkey: text('rkey').notNull(), 7 + skillName: text('skill_name').notNull(), 8 + category: text('category'), 9 + createdAt: timestamp('created_at', { withTimezone: true }).notNull(), 10 + indexedAt: timestamp('indexed_at', { withTimezone: true }).notNull().defaultNow(), 11 + }, (table) => [ 12 + primaryKey({ columns: [table.did, table.rkey] }), 13 + ]);
+31
tests/db/schema.test.ts
··· 1 + import { describe, it, expect } from 'vitest'; 2 + import { profiles, positions, education, skills } from '../../src/db/schema/index.js'; 3 + 4 + describe('Core schema tables', () => { 5 + it('profiles table has expected columns', () => { 6 + expect(profiles.did).toBeDefined(); 7 + expect(profiles.handle).toBeDefined(); 8 + expect(profiles.headline).toBeDefined(); 9 + expect(profiles.about).toBeDefined(); 10 + expect(profiles.indexedAt).toBeDefined(); 11 + }); 12 + 13 + it('positions table references profiles via did', () => { 14 + expect(positions.did).toBeDefined(); 15 + expect(positions.rkey).toBeDefined(); 16 + expect(positions.companyName).toBeDefined(); 17 + expect(positions.title).toBeDefined(); 18 + }); 19 + 20 + it('education table references profiles via did', () => { 21 + expect(education.did).toBeDefined(); 22 + expect(education.rkey).toBeDefined(); 23 + expect(education.institution).toBeDefined(); 24 + }); 25 + 26 + it('skills table references profiles via did', () => { 27 + expect(skills.did).toBeDefined(); 28 + expect(skills.rkey).toBeDefined(); 29 + expect(skills.skillName).toBeDefined(); 30 + }); 31 + });