A lexicon-driven AppView for ATProto.
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: migrate legacy script/index_hook data to scripts table and drop columns

Signed-off-by: Trezy <tre@trezy.com>

Signed-off-by: Trezy <tre@trezy.com>

+81 -312
+26
migrations/postgres/20260502000001_migrate_legacy_scripts.sql
··· 1 + -- Migrate legacy index_hook and script columns into the trigger-keyed 2 + -- scripts table, then drop the old columns. 3 + 4 + -- 1. index_hook -> record.index:<lexicon_id> 5 + INSERT INTO scripts (id, body, script_type, created_at, updated_at) 6 + SELECT 'record.index:' || id, index_hook, 'lua', NOW(), NOW() 7 + FROM lexicons 8 + WHERE index_hook IS NOT NULL 9 + ON CONFLICT (id) DO NOTHING; 10 + 11 + -- 2. script -> xrpc.query:<id> or xrpc.procedure:<id> 12 + INSERT INTO scripts (id, body, script_type, created_at, updated_at) 13 + SELECT 'xrpc.' || 14 + CASE (lexicon_json::jsonb)->'defs'->'main'->>'type' 15 + WHEN 'query' THEN 'query' 16 + WHEN 'procedure' THEN 'procedure' 17 + END || ':' || id, 18 + script, 'lua', NOW(), NOW() 19 + FROM lexicons 20 + WHERE script IS NOT NULL 21 + AND (lexicon_json::jsonb)->'defs'->'main'->>'type' IN ('query', 'procedure') 22 + ON CONFLICT (id) DO NOTHING; 23 + 24 + -- 3. Drop legacy columns. 25 + ALTER TABLE lexicons DROP COLUMN index_hook; 26 + ALTER TABLE lexicons DROP COLUMN script;
+26
migrations/sqlite/20260502000001_migrate_legacy_scripts.sql
··· 1 + -- Migrate legacy index_hook and script columns into the trigger-keyed 2 + -- scripts table, then drop the old columns. 3 + 4 + -- 1. index_hook -> record.index:<lexicon_id> 5 + INSERT INTO scripts (id, body, script_type, created_at, updated_at) 6 + SELECT 'record.index:' || id, index_hook, 'lua', datetime('now'), datetime('now') 7 + FROM lexicons 8 + WHERE index_hook IS NOT NULL 9 + ON CONFLICT (id) DO NOTHING; 10 + 11 + -- 2. script -> xrpc.query:<id> or xrpc.procedure:<id> 12 + INSERT INTO scripts (id, body, script_type, created_at, updated_at) 13 + SELECT 'xrpc.' || 14 + CASE json_extract(lexicon_json, '$.defs.main.type') 15 + WHEN 'query' THEN 'query' 16 + WHEN 'procedure' THEN 'procedure' 17 + END || ':' || id, 18 + script, 'lua', datetime('now'), datetime('now') 19 + FROM lexicons 20 + WHERE script IS NOT NULL 21 + AND json_extract(lexicon_json, '$.defs.main.type') IN ('query', 'procedure') 22 + ON CONFLICT (id) DO NOTHING; 23 + 24 + -- 3. Drop legacy columns. 25 + ALTER TABLE lexicons DROP COLUMN index_hook; 26 + ALTER TABLE lexicons DROP COLUMN script;
+6 -52
src/admin/lexicons.rs
··· 60 60 1, 61 61 body.target_collection.clone(), 62 62 action.clone(), 63 - body.script.clone(), 64 - body.index_hook.clone(), 65 63 body.token_cost.map(|c| c as u32), 66 64 ) 67 65 .map_err(|e| AppError::BadRequest(format!("failed to parse lexicon: {e}")))?; 68 66 69 - // Validate script if provided 70 - if let Some(ref script) = body.script { 71 - crate::lua::validate_script(script).map_err(AppError::BadRequest)?; 72 - } 73 - 74 - // Validate index_hook if provided 75 - if let Some(ref script) = body.index_hook { 76 - crate::lua::validate_script(script).map_err(AppError::BadRequest)?; 77 - } 78 - 79 67 let action_str = action.to_optional_str(); 80 - let has_script = body.script.is_some(); 81 68 let lexicon_json_str = serde_json::to_string(&body.lexicon_json).unwrap_or_default(); 82 69 let now = now_rfc3339(); 83 70 84 71 // Upsert into database 85 72 let sql = adapt_sql( 86 73 r#" 87 - INSERT INTO lexicons (id, lexicon_json, backfill, target_collection, action, script, index_hook, token_cost, source, created_at) 88 - VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'manual', ?) 74 + INSERT INTO lexicons (id, lexicon_json, backfill, target_collection, action, token_cost, source, created_at) 75 + VALUES (?, ?, ?, ?, ?, ?, 'manual', ?) 89 76 ON CONFLICT (id) DO UPDATE SET 90 77 lexicon_json = EXCLUDED.lexicon_json, 91 78 backfill = EXCLUDED.backfill, 92 79 target_collection = EXCLUDED.target_collection, 93 80 action = EXCLUDED.action, 94 - script = EXCLUDED.script, 95 - index_hook = EXCLUDED.index_hook, 96 81 token_cost = EXCLUDED.token_cost, 97 82 source = 'manual', 98 83 revision = lexicons.revision + 1, ··· 107 92 .bind(if body.backfill { 1_i32 } else { 0_i32 }) 108 93 .bind(&body.target_collection) 109 94 .bind(action_str) 110 - .bind(&body.script) 111 - .bind(&body.index_hook) 112 95 .bind(body.token_cost) 113 96 .bind(&now) 114 97 .bind(&now) ··· 124 107 revision, 125 108 body.target_collection, 126 109 action, 127 - body.script, 128 - body.index_hook.clone(), 129 110 body.token_cost.map(|c| c as u32), 130 111 ) 131 112 .map_err(|e| AppError::Internal(format!("failed to re-parse lexicon: {e}")))?; ··· 156 137 subject: Some(id.clone()), 157 138 detail: serde_json::json!({ 158 139 "revision": revision, 159 - "has_script": has_script, 160 - "has_index_hook": body.index_hook.is_some(), 161 140 "source": "manual", 162 141 }), 163 142 }, ··· 182 161 auth.require(Permission::LexiconsRead).await?; 183 162 let backend = state.db_backend; 184 163 let sql = adapt_sql( 185 - "SELECT id, revision, lexicon_json, backfill, action, target_collection, script, index_hook, source, authority_did, last_fetched_at, created_at, updated_at, token_cost FROM lexicons ORDER BY id", 164 + "SELECT id, revision, lexicon_json, backfill, action, target_collection, source, authority_did, last_fetched_at, created_at, updated_at, token_cost FROM lexicons ORDER BY id", 186 165 backend, 187 166 ); 188 167 #[allow(clippy::type_complexity)] ··· 191 170 i32, 192 171 String, 193 172 i32, 194 - Option<String>, 195 - Option<String>, 196 173 Option<String>, 197 174 Option<String>, 198 175 String, ··· 216 193 backfill, 217 194 action, 218 195 target_collection, 219 - script, 220 - index_hook, 221 196 source, 222 197 authority_did, 223 198 last_fetched_at, ··· 226 201 token_cost, 227 202 )| { 228 203 let json: Value = serde_json::from_str(&json_str).unwrap_or_default(); 229 - let parsed = ParsedLexicon::parse( 230 - json, 231 - revision, 232 - None, 233 - ProcedureAction::Upsert, 234 - None, 235 - None, 236 - None, 237 - ); 204 + let parsed = 205 + ParsedLexicon::parse(json, revision, None, ProcedureAction::Upsert, None); 238 206 let lexicon_type = parsed 239 207 .as_ref() 240 208 .map(|p| format!("{:?}", p.lexicon_type).to_lowercase()) ··· 251 219 backfill: backfill != 0, 252 220 action, 253 221 target_collection, 254 - has_script: script.is_some(), 255 - has_index_hook: index_hook.is_some(), 256 222 source, 257 223 authority_did, 258 224 last_fetched_at, ··· 277 243 auth.require(Permission::LexiconsRead).await?; 278 244 let backend = state.db_backend; 279 245 let sql = adapt_sql( 280 - "SELECT id, revision, lexicon_json, backfill, action, target_collection, script, index_hook, source, authority_did, last_fetched_at, created_at, updated_at, token_cost FROM lexicons WHERE id = ?", 246 + "SELECT id, revision, lexicon_json, backfill, action, target_collection, source, authority_did, last_fetched_at, created_at, updated_at, token_cost FROM lexicons WHERE id = ?", 281 247 backend, 282 248 ); 283 249 #[allow(clippy::type_complexity)] ··· 286 252 i32, 287 253 String, 288 254 i32, 289 - Option<String>, 290 - Option<String>, 291 255 Option<String>, 292 256 Option<String>, 293 257 String, ··· 309 273 backfill, 310 274 action, 311 275 target_collection, 312 - script, 313 - index_hook, 314 276 source, 315 277 authority_did, 316 278 last_fetched_at, ··· 327 289 None, 328 290 ProcedureAction::Upsert, 329 291 None, 330 - None, 331 - None, 332 292 ) 333 293 .map(|p| format!("{:?}", p.lexicon_type).to_lowercase()) 334 294 .unwrap_or_else(|_| "unknown".into()); 335 - 336 - let has_script = script.is_some(); 337 295 338 296 Ok(Json(serde_json::json!({ 339 297 "id": id, ··· 343 301 "backfill": backfill != 0, 344 302 "action": action, 345 303 "target_collection": target_collection, 346 - "has_script": has_script, 347 - "script": script, 348 - "has_index_hook": index_hook.is_some(), 349 - "index_hook": index_hook, 350 304 "source": source, 351 305 "authority_did": authority_did, 352 306 "last_fetched_at": last_fetched_at,
-4
src/admin/network_lexicons.rs
··· 72 72 body.target_collection.clone(), 73 73 ProcedureAction::Upsert, 74 74 None, 75 - None, 76 - None, 77 75 ) 78 76 .map_err(|e| AppError::BadRequest(format!("failed to parse lexicon: {e}")))?; 79 77 ··· 120 118 revision, 121 119 body.target_collection, 122 120 ProcedureAction::Upsert, 123 - None, 124 - None, 125 121 None, 126 122 ) 127 123 .map_err(|e| AppError::Internal(format!("failed to re-parse lexicon: {e}")))?;
-4
src/admin/types.rs
··· 13 13 pub(super) backfill: bool, 14 14 pub(super) action: Option<String>, 15 15 pub(super) target_collection: Option<String>, 16 - pub(super) has_script: bool, 17 - pub(super) has_index_hook: bool, 18 16 pub(super) source: String, 19 17 pub(super) authority_did: Option<String>, 20 18 pub(super) last_fetched_at: Option<String>, ··· 33 31 pub(super) backfill: bool, 34 32 pub(super) target_collection: Option<String>, 35 33 pub(super) action: Option<String>, 36 - pub(super) script: Option<String>, 37 - pub(super) index_hook: Option<String>, 38 34 pub(super) token_cost: Option<i32>, 39 35 } 40 36
+9 -160
src/lexicon.rs
··· 79 79 pub target_collection: Option<String>, 80 80 /// For procedures: the action this procedure performs (create, update, delete, upsert). 81 81 pub action: ProcedureAction, 82 - /// Optional Lua script that replaces the built-in handler. 83 - pub script: Option<String>, 84 - /// Optional Lua script that runs when a record in this collection is indexed. 85 - pub index_hook: Option<String>, 86 82 /// Optional per-NSID token cost for rate limiting. 87 83 pub token_cost: Option<u32>, 88 84 /// Optional space type NSID indicating this lexicon is designed for use within spaces of that type. ··· 96 92 revision: i32, 97 93 target_collection: Option<String>, 98 94 action: ProcedureAction, 99 - script: Option<String>, 100 - index_hook: Option<String>, 101 95 token_cost: Option<u32>, 102 96 ) -> Result<Self, String> { 103 97 let id = raw ··· 146 140 revision, 147 141 target_collection, 148 142 action, 149 - script, 150 - index_hook, 151 143 token_cost, 152 144 space_type, 153 145 }) ··· 182 174 i32, 183 175 Option<String>, 184 176 Option<String>, 185 - Option<String>, 186 - Option<String>, 187 177 Option<i32>, 188 178 )> = sqlx::query_as( 189 - "SELECT id, lexicon_json, revision, target_collection, action, script, index_hook, token_cost FROM lexicons", 179 + "SELECT id, lexicon_json, revision, target_collection, action, token_cost FROM lexicons", 190 180 ) 191 181 .fetch_all(db) 192 182 .await ··· 196 186 inner.clear(); 197 187 198 188 let mut loaded = 0u32; 199 - for ( 200 - id, 201 - json_str, 202 - revision, 203 - target_collection, 204 - action_str, 205 - script, 206 - index_hook, 207 - token_cost, 208 - ) in rows 209 - { 189 + for (id, json_str, revision, target_collection, action_str, token_cost) in rows { 210 190 let json: Value = match serde_json::from_str(&json_str) { 211 191 Ok(v) => v, 212 192 Err(e) => { ··· 226 206 revision, 227 207 target_collection, 228 208 action, 229 - script, 230 - index_hook, 231 209 token_cost.map(|c| c as u32), 232 210 ) { 233 211 Ok(parsed) => { ··· 290 268 .filter(|lex| lex.lexicon_type == LexiconType::Procedure) 291 269 .map(|lex| lex.id.clone()) 292 270 .collect() 293 - } 294 - 295 - /// Get the index_hook for a record-type lexicon by its collection NSID. 296 - pub async fn get_index_hook(&self, collection: &str) -> Option<String> { 297 - let inner = self.inner.read().await; 298 - inner.get(collection).and_then(|lex| lex.index_hook.clone()) 299 271 } 300 272 301 273 /// Return the total count of registered lexicons. ··· 393 365 None, 394 366 ProcedureAction::Upsert, 395 367 None, 396 - None, 397 - None, 398 368 ) 399 369 .unwrap(); 400 370 assert_eq!(parsed.id, "games.gamesgamesgamesgames.game"); ··· 412 382 2, 413 383 Some("games.gamesgamesgamesgames.game".into()), 414 384 ProcedureAction::Upsert, 415 - None, 416 - None, 417 385 None, 418 386 ) 419 387 .unwrap(); ··· 434 402 1, 435 403 None, 436 404 ProcedureAction::Upsert, 437 - None, 438 - None, 439 405 None, 440 406 ) 441 407 .unwrap(); ··· 452 418 None, 453 419 ProcedureAction::Delete, 454 420 None, 455 - None, 456 - None, 457 421 ) 458 422 .unwrap(); 459 423 assert_eq!(parsed.action, ProcedureAction::Delete); ··· 467 431 None, 468 432 ProcedureAction::Upsert, 469 433 None, 470 - None, 471 - None, 472 434 ) 473 435 .unwrap(); 474 436 assert_eq!(parsed.lexicon_type, LexiconType::Definitions); ··· 477 439 #[test] 478 440 fn parse_missing_id_returns_error() { 479 441 let raw = json!({"lexicon": 1, "defs": {}}); 480 - let result = ParsedLexicon::parse(raw, 1, None, ProcedureAction::Upsert, None, None, None); 442 + let result = ParsedLexicon::parse(raw, 1, None, ProcedureAction::Upsert, None); 481 443 assert!(result.is_err()); 482 444 assert!(result.unwrap_err().contains("id")); 483 445 } ··· 485 447 #[test] 486 448 fn parse_preserves_raw_json() { 487 449 let raw = record_lexicon_json(); 488 - let parsed = ParsedLexicon::parse( 489 - raw.clone(), 490 - 1, 491 - None, 492 - ProcedureAction::Upsert, 493 - None, 494 - None, 495 - None, 496 - ) 497 - .unwrap(); 450 + let parsed = 451 + ParsedLexicon::parse(raw.clone(), 1, None, ProcedureAction::Upsert, None).unwrap(); 498 452 assert_eq!(parsed.raw, raw); 499 453 } 500 454 ··· 505 459 1, 506 460 Some("custom.collection".into()), 507 461 ProcedureAction::Upsert, 508 - None, 509 - None, 510 462 None, 511 463 ) 512 464 .unwrap(); ··· 532 484 None, 533 485 ProcedureAction::Upsert, 534 486 None, 535 - None, 536 - None, 537 487 ) 538 488 .unwrap(); 539 489 reg.upsert(parsed).await; ··· 552 502 None, 553 503 ProcedureAction::Upsert, 554 504 None, 555 - None, 556 - None, 557 505 ) 558 506 .unwrap(); 559 507 reg.upsert(v1).await; ··· 563 511 5, 564 512 None, 565 513 ProcedureAction::Upsert, 566 - None, 567 - None, 568 514 None, 569 515 ) 570 516 .unwrap(); ··· 589 535 None, 590 536 ProcedureAction::Upsert, 591 537 None, 592 - None, 593 - None, 594 538 ) 595 539 .unwrap(); 596 540 reg.upsert(parsed).await; ··· 621 565 None, 622 566 ProcedureAction::Upsert, 623 567 None, 624 - None, 625 - None, 626 568 ) 627 569 .unwrap(); 628 - let query = ParsedLexicon::parse( 629 - query_lexicon_json(), 630 - 1, 631 - None, 632 - ProcedureAction::Upsert, 633 - None, 634 - None, 635 - None, 636 - ) 637 - .unwrap(); 570 + let query = 571 + ParsedLexicon::parse(query_lexicon_json(), 1, None, ProcedureAction::Upsert, None) 572 + .unwrap(); 638 573 let procedure = ParsedLexicon::parse( 639 574 procedure_lexicon_json(), 640 575 1, 641 576 None, 642 577 ProcedureAction::Upsert, 643 - None, 644 - None, 645 578 None, 646 579 ) 647 580 .unwrap(); ··· 650 583 1, 651 584 None, 652 585 ProcedureAction::Upsert, 653 - None, 654 - None, 655 586 None, 656 587 ) 657 588 .unwrap(); ··· 723 654 assert_eq!(ProcedureAction::Upsert.to_optional_str(), None); 724 655 } 725 656 726 - // ----------------------------------------------------------------------- 727 - // index_hook 728 - // ----------------------------------------------------------------------- 729 - 730 - #[test] 731 - fn parse_preserves_index_hook() { 732 - let parsed = ParsedLexicon::parse( 733 - record_lexicon_json(), 734 - 1, 735 - None, 736 - ProcedureAction::Upsert, 737 - None, 738 - Some("function handle() end".into()), 739 - None, 740 - ) 741 - .unwrap(); 742 - assert_eq!(parsed.index_hook, Some("function handle() end".into())); 743 - } 744 - 745 - #[test] 746 - fn parse_index_hook_none_by_default() { 747 - let parsed = ParsedLexicon::parse( 748 - record_lexicon_json(), 749 - 1, 750 - None, 751 - ProcedureAction::Upsert, 752 - None, 753 - None, 754 - None, 755 - ) 756 - .unwrap(); 757 - assert!(parsed.index_hook.is_none()); 758 - } 759 - 760 - #[tokio::test] 761 - async fn registry_get_index_hook_returns_script() { 762 - let reg = LexiconRegistry::new(); 763 - let parsed = ParsedLexicon::parse( 764 - record_lexicon_json(), 765 - 1, 766 - None, 767 - ProcedureAction::Upsert, 768 - None, 769 - Some("function handle() log('hook') end".into()), 770 - None, 771 - ) 772 - .unwrap(); 773 - reg.upsert(parsed).await; 774 - 775 - let script = reg.get_index_hook("games.gamesgamesgamesgames.game").await; 776 - assert_eq!(script, Some("function handle() log('hook') end".into())); 777 - } 778 - 779 - #[tokio::test] 780 - async fn registry_get_index_hook_returns_none_when_absent() { 781 - let reg = LexiconRegistry::new(); 782 - let parsed = ParsedLexicon::parse( 783 - record_lexicon_json(), 784 - 1, 785 - None, 786 - ProcedureAction::Upsert, 787 - None, 788 - None, 789 - None, 790 - ) 791 - .unwrap(); 792 - reg.upsert(parsed).await; 793 - 794 - let script = reg.get_index_hook("games.gamesgamesgamesgames.game").await; 795 - assert!(script.is_none()); 796 - } 797 - 798 - #[tokio::test] 799 - async fn registry_get_index_hook_returns_none_for_unknown() { 800 - let reg = LexiconRegistry::new(); 801 - let script = reg.get_index_hook("nonexistent").await; 802 - assert!(script.is_none()); 803 - } 804 - 805 657 #[test] 806 658 fn parse_space_type_from_lexicon() { 807 659 let raw = json!({ ··· 821 673 } 822 674 } 823 675 }); 824 - let parsed = 825 - ParsedLexicon::parse(raw, 1, None, ProcedureAction::Upsert, None, None, None).unwrap(); 676 + let parsed = ParsedLexicon::parse(raw, 1, None, ProcedureAction::Upsert, None).unwrap(); 826 677 assert_eq!(parsed.space_type.as_deref(), Some("com.example.forum")); 827 678 } 828 679 ··· 833 684 1, 834 685 None, 835 686 ProcedureAction::Upsert, 836 - None, 837 - None, 838 687 None, 839 688 ) 840 689 .unwrap();
+1 -2
src/lua/scripts.rs
··· 415 415 register_default_apis(&lua, &state_arc, &script.id, Some(payload.did))?; 416 416 417 417 // Legacy globals (action, uri, did, collection, rkey, record) for 418 - // back-compat with scripts written against the old `index_hook` 419 - // surface. 418 + // convenience / backwards compatibility. 420 419 context::set_hook_context( 421 420 &lua, 422 421 payload.action,
+8 -12
src/lua/xrpc_api.rs
··· 336 336 .unwrap(); 337 337 } 338 338 339 - fn make_query_lexicon(id: &str, script: Option<&str>) -> ParsedLexicon { 339 + fn make_query_lexicon(id: &str) -> ParsedLexicon { 340 340 ParsedLexicon { 341 341 id: id.to_string(), 342 342 lexicon_type: LexiconType::Query, ··· 349 349 revision: 1, 350 350 target_collection: None, 351 351 action: ProcedureAction::Create, 352 - script: script.map(|s| s.to_string()), 353 - index_hook: None, 354 352 token_cost: None, 355 353 space_type: None, 356 354 } 357 355 } 358 356 359 - fn make_procedure_lexicon(id: &str, script: Option<&str>) -> ParsedLexicon { 357 + fn make_procedure_lexicon(id: &str) -> ParsedLexicon { 360 358 ParsedLexicon { 361 359 id: id.to_string(), 362 360 lexicon_type: LexiconType::Procedure, ··· 369 367 revision: 1, 370 368 target_collection: None, 371 369 action: ProcedureAction::Create, 372 - script: script.map(|s| s.to_string()), 373 - index_hook: None, 374 370 token_cost: None, 375 371 space_type: None, 376 372 } ··· 463 459 let state = test_state(); 464 460 465 461 // Register a scripted query that returns a static response 466 - let lexicon = make_query_lexicon("test.echo", None); 462 + let lexicon = make_query_lexicon("test.echo"); 467 463 state.lexicons.upsert(lexicon).await; 468 464 seed_script( 469 465 &state, ··· 488 484 async fn query_local_script_receives_params() { 489 485 let state = test_state(); 490 486 491 - let lexicon = make_query_lexicon("test.greet", None); 487 + let lexicon = make_query_lexicon("test.greet"); 492 488 state.lexicons.upsert(lexicon).await; 493 489 seed_script( 494 490 &state, ··· 517 513 async fn query_local_script_receives_caller_did() { 518 514 let state = test_state(); 519 515 520 - let lexicon = make_query_lexicon("test.whoami", None); 516 + let lexicon = make_query_lexicon("test.whoami"); 521 517 state.lexicons.upsert(lexicon).await; 522 518 seed_script( 523 519 &state, ··· 562 558 async fn query_rejects_procedure_lexicon() { 563 559 let state = test_state(); 564 560 565 - let lexicon = make_procedure_lexicon("test.create", None); 561 + let lexicon = make_procedure_lexicon("test.create"); 566 562 state.lexicons.upsert(lexicon).await; 567 563 568 564 let mut params = HashMap::new(); ··· 576 572 async fn procedure_rejects_query_lexicon() { 577 573 let state = test_state(); 578 574 579 - let lexicon = make_query_lexicon("test.echo", Some("function handle() end")); 575 + let lexicon = make_query_lexicon("test.echo"); 580 576 state.lexicons.upsert(lexicon).await; 581 577 582 578 let claims = Claims::internal("did:plc:test".into()); ··· 597 593 let state = test_state(); 598 594 599 595 // Register a simple query that the outer script will call 600 - let inner_lexicon = make_query_lexicon("test.inner", None); 596 + let inner_lexicon = make_query_lexicon("test.inner"); 601 597 state.lexicons.upsert(inner_lexicon).await; 602 598 seed_script( 603 599 &state,
-2
src/main.rs
··· 138 138 target_collection.clone(), 139 139 ProcedureAction::Upsert, 140 140 None, 141 - None, 142 - None, 143 141 ) { 144 142 Ok(parsed) => { 145 143 let now = db::now_rfc3339();
-6
src/oauth/client_auth.rs
··· 375 375 None, 376 376 crate::lexicon::ProcedureAction::Upsert, 377 377 None, 378 - None, 379 - None, 380 378 ) 381 379 .unwrap(); 382 380 reg.upsert(parsed).await; ··· 435 433 None, 436 434 crate::lexicon::ProcedureAction::Upsert, 437 435 None, 438 - None, 439 - None, 440 436 ) 441 437 .unwrap(); 442 438 reg.upsert(parsed).await; ··· 486 482 1, 487 483 None, 488 484 crate::lexicon::ProcedureAction::Upsert, 489 - None, 490 - None, 491 485 None, 492 486 ) 493 487 .unwrap();
-2
src/record_handler.rs
··· 303 303 target_collection.clone(), 304 304 ProcedureAction::Upsert, 305 305 None, 306 - None, 307 - None, 308 306 ) { 309 307 Ok(p) => p, 310 308 Err(e) => {
+1 -2
src/xrpc/procedure.rs
··· 19 19 lexicon: &crate::lexicon::ParsedLexicon, 20 20 ) -> Result<Response, AppError> { 21 21 // Trigger-keyed dispatch: a script bound at `xrpc.procedure:<id>` 22 - // overrides the default PDS-write flow. The legacy `lexicon.script` 23 - // column is no longer read. 22 + // overrides the default PDS-write flow. 24 23 let trigger = format!("xrpc.procedure:{}", lexicon.id); 25 24 if let Some(resolved) = crate::lua::resolve(state, &trigger).await { 26 25 // Delegation guard preserved from origin/dev: scripts that run
+1 -2
src/xrpc/query.rs
··· 16 16 claims: Option<&Claims>, 17 17 ) -> Result<Response, AppError> { 18 18 // Trigger-keyed dispatch: a script bound at `xrpc.query:<id>` 19 - // overrides the default list / get-record flow. The legacy 20 - // `lexicon.script` column is no longer read. 19 + // overrides the default list / get-record flow. 21 20 let trigger = format!("xrpc.query:{}", lexicon.id); 22 21 if let Some(resolved) = crate::lua::resolve(state, &trigger).await { 23 22 return crate::lua::execute_query_script(
+1 -13
web/src/app/dashboard/lexicons/[id]/lexicon-detail.tsx
··· 44 44 const [deleting, setDeleting] = useState(false); 45 45 const [saving, setSaving] = useState(false); 46 46 47 - // Editable text state. The lexicon page used to edit `script` and 48 - // `index_hook` columns inline via lua editors; those columns are 49 - // now managed via the Scripts subsystem (see "Scripts targeting 50 - // this lexicon" panel below). We pass the existing values through 51 - // unchanged on save so legacy data isn't accidentally NULLed. 52 47 const [jsonText, setJsonText] = useState(""); 53 48 const [originalJson, setOriginalJson] = useState(""); 54 49 const [tokenCost, setTokenCost] = useState(""); ··· 89 84 await uploadLexicon({ 90 85 lexicon_json: lexiconJson, 91 86 backfill: lexicon.backfill, 92 - // Preserve any legacy script / index_hook values verbatim — we 93 - // no longer edit them here, but leaving them out of the body 94 - // would NULL the columns on upsert and lose data. 95 - script: lexicon.script ?? undefined, 96 - index_hook: lexicon.index_hook ?? undefined, 97 87 token_cost: tokenCost ? Number(tokenCost) : null, 98 88 }); 99 89 load(); ··· 262 252 263 253 {/* JSON editor only — scripts (record-event handlers, XRPC 264 254 handlers, label-arrival handlers) are managed via the 265 - "Scripts targeting this lexicon" panel above. The legacy 266 - `script` / `index_hook` columns on the lexicons table are 267 - preserved as-is on save but no longer edited here. */} 255 + "Scripts targeting this lexicon" panel above. */} 268 256 <CodePanels 269 257 className="flex-1 min-h-0 px-4 md:px-6" 270 258 jsonValue={jsonText}
+2 -31
web/src/app/dashboard/lexicons/new/page.tsx
··· 9 9 resolveNetworkLexicon, 10 10 uploadLexicon, 11 11 } from "@/lib/api"; 12 - import { LEXICON_TEMPLATE, procedureScript, queryScript } from "@/lib/lua-templates"; 13 - import { useLuaCompletions } from "@/hooks/use-lua-completions"; 12 + import { resolveNsid } from "@/lib/nsid"; 13 + import { LEXICON_TEMPLATE } from "@/lib/lua-templates"; 14 14 import { CodePanels } from "@/components/code-panels"; 15 15 import { SiteHeader } from "@/components/site-header"; 16 16 import { Button } from "@/components/ui/button"; ··· 35 35 // Local state 36 36 const [json, setJson] = useState(LEXICON_TEMPLATE); 37 37 const [localTargetCollection, setLocalTargetCollection] = useState(""); 38 - const [script, setScript] = useState(""); 39 38 const [backfill, setBackfill] = useState(true); 40 - const scriptManuallyEdited = useRef(false); 41 - 42 - // Collections for Record() completions and record schemas 43 - const { luaCompletions, collections } = useLuaCompletions(json); 44 39 45 40 // Network state 46 41 const [nsid, setNsid] = useState(""); ··· 64 59 65 60 const showLocalTargetCollection = 66 61 localMainType === "query" || localMainType === "procedure"; 67 - const showScript = localMainType === "query" || localMainType === "procedure"; 68 - 69 - // Auto-generate script when type or target collection changes 70 - useEffect(() => { 71 - if (scriptManuallyEdited.current) return; 72 - if (localMainType === "procedure") { 73 - setScript(procedureScript(localTargetCollection)); 74 - } else if (localMainType === "query") { 75 - setScript(queryScript(localTargetCollection)); 76 - } 77 - }, [localMainType, localTargetCollection]); 78 - 79 - function handleScriptChange(value: string) { 80 - scriptManuallyEdited.current = true; 81 - setScript(value); 82 - } 83 - 84 - // Reset manual-edit flag when type changes 85 62 const prevType = useRef(localMainType); 86 63 useEffect(() => { 87 64 if (prevType.current !== localMainType) { 88 - scriptManuallyEdited.current = false; 89 65 prevType.current = localMainType; 90 66 } 91 67 }, [localMainType]); ··· 139 115 await uploadLexicon({ 140 116 lexicon_json: lexiconJson, 141 117 backfill: localMainType === "record" && backfill, 142 - script: showScript && script ? script : undefined, 143 118 }); 144 119 router.push("/dashboard/lexicons"); 145 120 } catch (e: unknown) { ··· 211 186 className="flex-1 min-h-0" 212 187 jsonValue={json} 213 188 onJsonChange={setJson} 214 - luaValue={showScript ? script : undefined} 215 - onLuaChange={showScript ? handleScriptChange : undefined} 216 - luaCompletions={showScript ? luaCompletions : undefined} 217 - collections={showScript ? collections : undefined} 218 189 /> 219 190 </div> 220 191
-14
web/src/app/dashboard/lexicons/page.tsx
··· 150 150 enableSorting: true, 151 151 }, 152 152 { 153 - id: "has_script", 154 - accessorKey: "has_script", 155 - header: ({ column }) => ( 156 - <DataTableColumnHeader column={column} label="Script" /> 157 - ), 158 - cell: ({ row }) => 159 - row.original.has_script ? ( 160 - <Badge variant="secondary">Lua</Badge> 161 - ) : ( 162 - "--" 163 - ), 164 - enableSorting: true, 165 - }, 166 - { 167 153 id: "backfill", 168 154 accessorKey: "backfill", 169 155 header: ({ column }) => (
-2
web/src/lib/api.ts
··· 141 141 backfill?: boolean; 142 142 target_collection?: string; 143 143 action?: string; 144 - script?: string; 145 - index_hook?: string; 146 144 token_cost?: number | null; 147 145 }) { 148 146 return apiFetch<{ id: string; revision: number }>("/admin/lexicons", {
-4
web/src/types/lexicons.ts
··· 5 5 backfill: boolean 6 6 action: string | null 7 7 target_collection: string | null 8 - has_script: boolean 9 - has_index_hook: boolean 10 8 source: string 11 9 authority_did: string | null 12 10 last_fetched_at: string | null ··· 17 15 18 16 export interface LexiconDetail extends LexiconSummary { 19 17 lexicon_json: Record<string, unknown> 20 - script: string | null 21 - index_hook: string | null 22 18 }