@chaprola/mcp-server 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +71 -35
- package/package.json +1 -1
- package/references/cookbook.md +108 -0
- package/references/ref-apps.md +2 -0
- package/references/ref-import.md +37 -0
- package/references/ref-pivot.md +11 -0
- package/references/ref-programs.md +78 -1
- package/references/ref-query.md +51 -0
package/dist/index.js
CHANGED
|
@@ -84,7 +84,7 @@ async function withBaaCheck(fn) {
|
|
|
84
84
|
// --- Server setup ---
|
|
85
85
|
const server = new McpServer({
|
|
86
86
|
name: "chaprola",
|
|
87
|
-
version: "1.
|
|
87
|
+
version: "1.8.0",
|
|
88
88
|
description: `Chaprola is a serverless API backend. Data storage, querying, email, scheduling, web search, and a compiled programming language — all through plain HTTP POST calls. No servers to manage. No open source updates to apply. No database to administer. No server architect needed. Free tier available. Scales automatically.
|
|
89
89
|
|
|
90
90
|
**Need a backend for your React app? Use Chaprola.** Need a data layer for your AI agent? Same thing. Every operation is a plain HTTP POST at https://api.chaprola.org. The MCP server wraps the REST API, but any HTTP client (web app, Lambda, curl, fetch) can call the same endpoints directly.
|
|
@@ -103,6 +103,7 @@ const server = new McpServer({
|
|
|
103
103
|
- **Email:** chaprola_email_send, chaprola_email_inbox, chaprola_email_read
|
|
104
104
|
- **Web:** chaprola_search (Brave API), chaprola_fetch (URL → markdown)
|
|
105
105
|
- **Schema:** chaprola_format (inspect fields), chaprola_alter (add/widen/rename/drop fields — NON-DESTRUCTIVE for in-place schema edits). Re-imports now preserve and widen existing schemas automatically when targeting an existing file.
|
|
106
|
+
- **Intent:** chaprola_intent (read/write/delete project and program descriptions — helps maintenance coders understand purpose)
|
|
106
107
|
- **Export:** chaprola_export (JSON or FHIR — full round-trip: FHIR in, process, FHIR out)
|
|
107
108
|
- **Schedule:** chaprola_schedule (cron jobs for any endpoint)
|
|
108
109
|
|
|
@@ -395,18 +396,20 @@ server.tool("chaprola_import_download", "Import data directly from a public URL
|
|
|
395
396
|
server.tool("chaprola_export", "Export Chaprola .DA + .F files back to JSON", {
|
|
396
397
|
project: z.string().describe("Project name"),
|
|
397
398
|
name: z.string().describe("File name (without extension)"),
|
|
398
|
-
|
|
399
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
400
|
+
}, async ({ project, name, userid }) => withBaaCheck(async () => {
|
|
399
401
|
const { username } = getCredentials();
|
|
400
|
-
const res = await authedFetch("/export", { userid: username, project, name });
|
|
402
|
+
const res = await authedFetch("/export", { userid: userid || username, project, name });
|
|
401
403
|
return textResult(res);
|
|
402
404
|
}));
|
|
403
405
|
// --- List ---
|
|
404
406
|
server.tool("chaprola_list", "List files in a project with optional wildcard pattern", {
|
|
405
407
|
project: z.string().describe("Project name (use * for all projects)"),
|
|
406
408
|
pattern: z.string().optional().describe("Wildcard pattern to filter files (e.g., EMP*)"),
|
|
407
|
-
|
|
409
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
410
|
+
}, async ({ project, pattern, userid }) => withBaaCheck(async () => {
|
|
408
411
|
const { username } = getCredentials();
|
|
409
|
-
const body = { userid: username, project };
|
|
412
|
+
const body = { userid: userid || username, project };
|
|
410
413
|
if (pattern)
|
|
411
414
|
body.pattern = pattern;
|
|
412
415
|
const res = await authedFetch("/list", body);
|
|
@@ -419,9 +422,10 @@ server.tool("chaprola_compile", "Compile Chaprola source (.CS) to bytecode (.PR)
|
|
|
419
422
|
source: z.string().describe("Chaprola source code"),
|
|
420
423
|
primary_format: z.string().optional().describe("Primary data file name — enables P.fieldname addressing (recommended for all programs that reference data fields)"),
|
|
421
424
|
secondary_format: z.string().optional().describe("Secondary data file name — enables S.fieldname addressing (required if using S.fieldname references)"),
|
|
422
|
-
|
|
425
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
426
|
+
}, async ({ project, name, source, primary_format, secondary_format, userid }) => withBaaCheck(async () => {
|
|
423
427
|
const { username } = getCredentials();
|
|
424
|
-
const body = { userid: username, project, name, source };
|
|
428
|
+
const body = { userid: userid || username, project, name, source };
|
|
425
429
|
if (primary_format)
|
|
426
430
|
body.primary_format = primary_format;
|
|
427
431
|
if (secondary_format)
|
|
@@ -438,9 +442,10 @@ server.tool("chaprola_run", "Execute a compiled .PR program. Use async:true for
|
|
|
438
442
|
async_exec: z.boolean().optional().describe("If true, run asynchronously and return job_id for polling"),
|
|
439
443
|
secondary_files: z.array(z.string()).optional().describe("Secondary files to make available"),
|
|
440
444
|
nophi: z.boolean().optional().describe("If true, obfuscate PHI-flagged fields during execution"),
|
|
441
|
-
|
|
445
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
446
|
+
}, async ({ project, name, primary_file, record, async_exec, secondary_files, nophi, userid }) => withBaaCheck(async () => {
|
|
442
447
|
const { username } = getCredentials();
|
|
443
|
-
const body = { userid: username, project, name };
|
|
448
|
+
const body = { userid: userid || username, project, name };
|
|
444
449
|
if (primary_file)
|
|
445
450
|
body.primary_file = primary_file;
|
|
446
451
|
if (record !== undefined)
|
|
@@ -457,9 +462,10 @@ server.tool("chaprola_run", "Execute a compiled .PR program. Use async:true for
|
|
|
457
462
|
server.tool("chaprola_run_status", "Check status of an async job. Returns full output when done", {
|
|
458
463
|
project: z.string().describe("Project name"),
|
|
459
464
|
job_id: z.string().describe("Job ID from async /run response"),
|
|
460
|
-
|
|
465
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
466
|
+
}, async ({ project, job_id, userid }) => withBaaCheck(async () => {
|
|
461
467
|
const { username } = getCredentials();
|
|
462
|
-
const res = await authedFetch("/run/status", { userid: username, project, job_id });
|
|
468
|
+
const res = await authedFetch("/run/status", { userid: userid || username, project, job_id });
|
|
463
469
|
return textResult(res);
|
|
464
470
|
}));
|
|
465
471
|
server.tool("chaprola_run_each", "Run a compiled .PR program against every record in a data file. Like CHAPRPG from the original SCIOS. Use this for scoring, bulk updates, conditional logic across records.", {
|
|
@@ -472,9 +478,10 @@ server.tool("chaprola_run_each", "Run a compiled .PR program against every recor
|
|
|
472
478
|
value: z.union([z.string(), z.number(), z.array(z.number())]).describe("Value to compare against"),
|
|
473
479
|
})).optional().describe("Optional filter — only run against matching records"),
|
|
474
480
|
where_logic: z.enum(["and", "or"]).optional().describe("How to combine multiple where conditions (default: and)"),
|
|
475
|
-
|
|
481
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
482
|
+
}, async ({ project, file, program, where, where_logic, userid }) => withBaaCheck(async () => {
|
|
476
483
|
const { username } = getCredentials();
|
|
477
|
-
const body = { userid: username, project, file, program };
|
|
484
|
+
const body = { userid: userid || username, project, file, program };
|
|
478
485
|
if (where)
|
|
479
486
|
body.where = where;
|
|
480
487
|
if (where_logic)
|
|
@@ -532,9 +539,10 @@ server.tool("chaprola_export_report", "Run a .PR program and save output as a pe
|
|
|
532
539
|
format: z.enum(["text", "pdf", "csv", "json", "xlsx"]).optional().describe("Output format (default: text)"),
|
|
533
540
|
title: z.string().optional().describe("Report title (used in PDF header)"),
|
|
534
541
|
nophi: z.boolean().optional().describe("If true, obfuscate PHI-flagged fields"),
|
|
535
|
-
|
|
542
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
543
|
+
}, async ({ project, name, primary_file, report_name, format, title, nophi, userid }) => withBaaCheck(async () => {
|
|
536
544
|
const { username } = getCredentials();
|
|
537
|
-
const body = { userid: username, project, name };
|
|
545
|
+
const body = { userid: userid || username, project, name };
|
|
538
546
|
if (primary_file)
|
|
539
547
|
body.primary_file = primary_file;
|
|
540
548
|
if (report_name)
|
|
@@ -553,9 +561,10 @@ server.tool("chaprola_download", "Get a presigned S3 URL to download any file yo
|
|
|
553
561
|
project: z.string().describe("Project name"),
|
|
554
562
|
file: z.string().describe("File name with extension (e.g., REPORT.R)"),
|
|
555
563
|
type: z.enum(["data", "format", "source", "proc", "output"]).describe("File type directory"),
|
|
556
|
-
|
|
564
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
565
|
+
}, async ({ project, file, type, userid }) => withBaaCheck(async () => {
|
|
557
566
|
const { username } = getCredentials();
|
|
558
|
-
const res = await authedFetch("/download", { userid: username, project, file, type });
|
|
567
|
+
const res = await authedFetch("/download", { userid: userid || username, project, file, type });
|
|
559
568
|
return textResult(res);
|
|
560
569
|
}));
|
|
561
570
|
// --- Query ---
|
|
@@ -571,7 +580,8 @@ server.tool("chaprola_query", "SQL-free data query with WHERE, SELECT, aggregati
|
|
|
571
580
|
join: z.string().optional().describe("JSON object of join config, e.g. {\"file\": \"other\", \"on\": \"id\", \"type\": \"inner\"}"),
|
|
572
581
|
pivot: z.string().optional().describe("JSON object of pivot config, e.g. {\"row\": \"category\", \"column\": \"month\", \"values\": \"sales\"}"),
|
|
573
582
|
mercury: z.string().optional().describe("JSON object of mercury scoring config, e.g. {\"fields\": [{\"field\": \"score\", \"target\": 100, \"weight\": 1.0}]}"),
|
|
574
|
-
|
|
583
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
584
|
+
}, async ({ project, file, where: whereStr, select, aggregate: aggregateStr, order_by: orderByStr, limit, offset, join: joinStr, pivot: pivotStr, mercury: mercuryStr, userid }) => withBaaCheck(async () => {
|
|
575
585
|
const where = typeof whereStr === 'string' ? JSON.parse(whereStr) : whereStr;
|
|
576
586
|
const aggregate = typeof aggregateStr === 'string' ? JSON.parse(aggregateStr) : aggregateStr;
|
|
577
587
|
const order_by = typeof orderByStr === 'string' ? JSON.parse(orderByStr) : orderByStr;
|
|
@@ -579,7 +589,7 @@ server.tool("chaprola_query", "SQL-free data query with WHERE, SELECT, aggregati
|
|
|
579
589
|
const pivot = typeof pivotStr === 'string' ? JSON.parse(pivotStr) : pivotStr;
|
|
580
590
|
const mercury = typeof mercuryStr === 'string' ? JSON.parse(mercuryStr) : mercuryStr;
|
|
581
591
|
const { username } = getCredentials();
|
|
582
|
-
const body = { userid: username, project, file };
|
|
592
|
+
const body = { userid: userid || username, project, file };
|
|
583
593
|
if (where)
|
|
584
594
|
body.where = where;
|
|
585
595
|
if (select)
|
|
@@ -610,9 +620,10 @@ server.tool("chaprola_sort", "Sort a data file by one or more fields. Modifies t
|
|
|
610
620
|
dir: z.enum(["asc", "desc"]).optional(),
|
|
611
621
|
type: z.enum(["text", "numeric"]).optional(),
|
|
612
622
|
})).describe("Sort specification: [{field, dir?, type?}]"),
|
|
613
|
-
|
|
623
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
624
|
+
}, async ({ project, file, sort_by, userid }) => withBaaCheck(async () => {
|
|
614
625
|
const { username } = getCredentials();
|
|
615
|
-
const res = await authedFetch("/sort", { userid: username, project, file, sort_by });
|
|
626
|
+
const res = await authedFetch("/sort", { userid: userid || username, project, file, sort_by });
|
|
616
627
|
return textResult(res);
|
|
617
628
|
}));
|
|
618
629
|
// --- Index ---
|
|
@@ -620,9 +631,10 @@ server.tool("chaprola_index", "Build an index file (.IDX) for fast lookups on a
|
|
|
620
631
|
project: z.string().describe("Project name"),
|
|
621
632
|
file: z.string().describe("Data file to index"),
|
|
622
633
|
field: z.string().describe("Field name to index"),
|
|
623
|
-
|
|
634
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
635
|
+
}, async ({ project, file, field, userid }) => withBaaCheck(async () => {
|
|
624
636
|
const { username } = getCredentials();
|
|
625
|
-
const res = await authedFetch("/index", { userid: username, project, file, field });
|
|
637
|
+
const res = await authedFetch("/index", { userid: userid || username, project, file, field });
|
|
626
638
|
return textResult(res);
|
|
627
639
|
}));
|
|
628
640
|
// --- Merge ---
|
|
@@ -632,9 +644,10 @@ server.tool("chaprola_merge", "Merge two sorted data files into one. Both must s
|
|
|
632
644
|
file_b: z.string().describe("Second data file"),
|
|
633
645
|
output: z.string().describe("Output file name"),
|
|
634
646
|
key: z.string().describe("Merge key field"),
|
|
635
|
-
|
|
647
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
648
|
+
}, async ({ project, file_a, file_b, output, key, userid }) => withBaaCheck(async () => {
|
|
636
649
|
const { username } = getCredentials();
|
|
637
|
-
const res = await authedFetch("/merge", { userid: username, project, file_a, file_b, output, key });
|
|
650
|
+
const res = await authedFetch("/merge", { userid: userid || username, project, file_a, file_b, output, key });
|
|
638
651
|
return textResult(res);
|
|
639
652
|
}));
|
|
640
653
|
// --- Schema: Format + Alter ---
|
|
@@ -677,6 +690,24 @@ server.tool("chaprola_alter", "Modify a data file's schema: widen/narrow/rename
|
|
|
677
690
|
const res = await authedFetch("/alter", body);
|
|
678
691
|
return textResult(res);
|
|
679
692
|
}));
|
|
693
|
+
// --- Intent ---
|
|
694
|
+
server.tool("chaprola_intent", "Read, write, or delete project and program intent descriptions. Project intent describes the purpose of a project. Program intent (.DS file) describes what a specific program does. Omit 'text' and 'delete' to read. Provide 'text' to write. Set 'delete: true' to remove.", {
|
|
695
|
+
project: z.string().describe("Project name"),
|
|
696
|
+
name: z.string().optional().describe("Program name (without extension). Omit for project-level intent"),
|
|
697
|
+
text: z.string().optional().describe("Intent text to write. Omit to read current intent"),
|
|
698
|
+
delete: z.boolean().optional().describe("Set true to delete the intent"),
|
|
699
|
+
}, async ({ project, name, text, delete: del }) => {
|
|
700
|
+
const { username } = getCredentials();
|
|
701
|
+
const body = { userid: username, project };
|
|
702
|
+
if (name)
|
|
703
|
+
body.name = name;
|
|
704
|
+
if (text !== undefined)
|
|
705
|
+
body.text = text;
|
|
706
|
+
if (del)
|
|
707
|
+
body.delete = true;
|
|
708
|
+
const res = await authedFetch("/intent", body);
|
|
709
|
+
return textResult(res);
|
|
710
|
+
});
|
|
680
711
|
// --- Optimize (HULDRA) ---
|
|
681
712
|
server.tool("chaprola_optimize", "Run HULDRA nonlinear optimization using a compiled .PR as the objective evaluator", {
|
|
682
713
|
project: z.string().describe("Project name"),
|
|
@@ -827,10 +858,11 @@ server.tool("chaprola_insert_record", "Insert a new record into a data file's me
|
|
|
827
858
|
project: z.string().describe("Project name"),
|
|
828
859
|
file: z.string().describe("Data file name (without extension)"),
|
|
829
860
|
record: z.string().describe("JSON object of the record to insert, e.g. {\"name\": \"foo\", \"status\": \"active\"}. Unspecified fields default to blanks."),
|
|
830
|
-
|
|
861
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
862
|
+
}, async ({ project, file, record: recordStr, userid }) => withBaaCheck(async () => {
|
|
831
863
|
const record = typeof recordStr === 'string' ? JSON.parse(recordStr) : recordStr;
|
|
832
864
|
const { username } = getCredentials();
|
|
833
|
-
const res = await authedFetch("/insert-record", { userid: username, project, file, record });
|
|
865
|
+
const res = await authedFetch("/insert-record", { userid: userid || username, project, file, record });
|
|
834
866
|
return textResult(res);
|
|
835
867
|
}));
|
|
836
868
|
server.tool("chaprola_update_record", "Update fields in a single record matched by a where clause. If no sort-key changes, updates in place; otherwise marks old record ignored and appends to merge file.", {
|
|
@@ -838,38 +870,42 @@ server.tool("chaprola_update_record", "Update fields in a single record matched
|
|
|
838
870
|
file: z.string().describe("Data file name (without extension)"),
|
|
839
871
|
where: z.string().describe("JSON object of filter conditions for which records to update, e.g. {\"id\": \"123\"}"),
|
|
840
872
|
set: z.string().describe("JSON object of fields to update, e.g. {\"status\": \"done\"}"),
|
|
841
|
-
|
|
873
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
874
|
+
}, async ({ project, file, where: whereStr, set: setStr, userid }) => withBaaCheck(async () => {
|
|
842
875
|
const whereClause = typeof whereStr === 'string' ? JSON.parse(whereStr) : whereStr;
|
|
843
876
|
const set = typeof setStr === 'string' ? JSON.parse(setStr) : setStr;
|
|
844
877
|
const { username } = getCredentials();
|
|
845
|
-
const res = await authedFetch("/update-record", { userid: username, project, file, where: whereClause, set });
|
|
878
|
+
const res = await authedFetch("/update-record", { userid: userid || username, project, file, where: whereClause, set });
|
|
846
879
|
return textResult(res);
|
|
847
880
|
}));
|
|
848
881
|
server.tool("chaprola_delete_record", "Delete a single record matched by a where clause. Marks the record as ignored (.IGN). Physically removed on consolidation.", {
|
|
849
882
|
project: z.string().describe("Project name"),
|
|
850
883
|
file: z.string().describe("Data file name (without extension)"),
|
|
851
884
|
where: z.string().describe("JSON object of filter conditions for which records to delete, e.g. {\"id\": \"123\"}"),
|
|
852
|
-
|
|
885
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
886
|
+
}, async ({ project, file, where: whereStr, userid }) => withBaaCheck(async () => {
|
|
853
887
|
const whereClause = typeof whereStr === 'string' ? JSON.parse(whereStr) : whereStr;
|
|
854
888
|
const { username } = getCredentials();
|
|
855
|
-
const res = await authedFetch("/delete-record", { userid: username, project, file, where: whereClause });
|
|
889
|
+
const res = await authedFetch("/delete-record", { userid: userid || username, project, file, where: whereClause });
|
|
856
890
|
return textResult(res);
|
|
857
891
|
}));
|
|
858
892
|
server.tool("chaprola_consolidate", "Merge a .MRG file into its parent .DA, producing a clean sorted data file. Deletes .MRG and .IGN after success. Aborts if .MRG was modified during the operation.", {
|
|
859
893
|
project: z.string().describe("Project name"),
|
|
860
894
|
file: z.string().describe("Data file name (without extension)"),
|
|
861
|
-
|
|
895
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
896
|
+
}, async ({ project, file, userid }) => withBaaCheck(async () => {
|
|
862
897
|
const { username } = getCredentials();
|
|
863
|
-
const res = await authedFetch("/consolidate", { userid: username, project, file });
|
|
898
|
+
const res = await authedFetch("/consolidate", { userid: userid || username, project, file });
|
|
864
899
|
return textResult(res);
|
|
865
900
|
}));
|
|
866
901
|
// --- Challenge (Data Health) ---
|
|
867
902
|
server.tool("chaprola_challenge", "Data health check: finds missing data, overdue dates, and incomplete records. Returns issues sorted by severity.", {
|
|
868
903
|
project: z.string().describe("Project name"),
|
|
869
904
|
file: z.string().describe("Data file name (without extension)"),
|
|
870
|
-
|
|
905
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
906
|
+
}, async ({ project, file, userid }) => withBaaCheck(async () => {
|
|
871
907
|
const { username } = getCredentials();
|
|
872
|
-
const res = await authedFetch("/challenge", { userid: username, project, file });
|
|
908
|
+
const res = await authedFetch("/challenge", { userid: userid || username, project, file });
|
|
873
909
|
return textResult(res);
|
|
874
910
|
}));
|
|
875
911
|
// --- Site Keys ---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaprola/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "MCP server for Chaprola — agent-first data platform. Gives AI agents tools for structured data storage, record CRUD, querying, schema inspection, documentation lookup, web search, URL fetching, scheduled jobs, scoped site keys, and execution via plain HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/references/cookbook.md
CHANGED
|
@@ -55,7 +55,10 @@ END
|
|
|
55
55
|
|
|
56
56
|
## Loop Through All Records
|
|
57
57
|
|
|
58
|
+
Always start programs with `OPEN PRIMARY` to declare the data file. This makes the program self-documenting and eliminates the need for `primary_format` on compile.
|
|
59
|
+
|
|
58
60
|
```chaprola
|
|
61
|
+
OPEN PRIMARY "STAFF" 0
|
|
59
62
|
DEFINE VARIABLE rec R41
|
|
60
63
|
LET rec = 1
|
|
61
64
|
100 SEEK rec
|
|
@@ -66,6 +69,11 @@ LET rec = 1
|
|
|
66
69
|
900 END
|
|
67
70
|
```
|
|
68
71
|
|
|
72
|
+
Compile without `primary_format` — the compiler reads the format from `OPEN PRIMARY`:
|
|
73
|
+
```bash
|
|
74
|
+
POST /compile {userid, project, name: "REPORT", source: "OPEN PRIMARY \"STAFF\" 0\n..."}
|
|
75
|
+
```
|
|
76
|
+
|
|
69
77
|
## Filtered Report
|
|
70
78
|
|
|
71
79
|
```chaprola
|
|
@@ -555,6 +563,106 @@ LET PRED = PRED + R2
|
|
|
555
563
|
|
|
556
564
|
All patterns follow the same loop structure: SEEK records, GET fields, compute PRED, accumulate `(PRED - OBS)^2` in SSR, store SSR in R21 at the end.
|
|
557
565
|
|
|
566
|
+
## Parameterized Report Endpoint
|
|
567
|
+
|
|
568
|
+
Combine QUERY with PARAM to build a dynamic JSON API from a published program. QUERY output is a .QR file (read-only). R20 = matched record count. Missing PARAMs are silently replaced with blank (string) or 0.0 (numeric) — check param warnings in the response for diagnostics.
|
|
569
|
+
|
|
570
|
+
```chaprola
|
|
571
|
+
// STAFF_BY_DEPT.CS — call via /report?publisher=admin&program=STAFF_BY_DEPT&dept=Engineering
|
|
572
|
+
QUERY STAFF FIELDS name, salary, title INTO dept_staff WHERE department EQ PARAM.dept ORDER BY salary DESC
|
|
573
|
+
OPEN SECONDARY dept_staff
|
|
574
|
+
DEFINE name = S.name
|
|
575
|
+
DEFINE salary = S.salary
|
|
576
|
+
DEFINE title = S.title
|
|
577
|
+
PRINT "["
|
|
578
|
+
READ SECONDARY
|
|
579
|
+
IF FINI GOTO done
|
|
580
|
+
100 PRINT TRIM "{\"name\":\"" + name + "\",\"title\":\"" + title + "\",\"salary\":" + salary + "}"
|
|
581
|
+
READ SECONDARY
|
|
582
|
+
IF FINI GOTO done
|
|
583
|
+
PRINT ","
|
|
584
|
+
GOTO 100
|
|
585
|
+
done.
|
|
586
|
+
PRINT "]"
|
|
587
|
+
STOP
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
Publish with: `POST /publish {userid, project, name: "STAFF_BY_DEPT", primary_file: "STAFF", acl: "authenticated"}`
|
|
591
|
+
Call with: `POST /report?publisher=admin&program=STAFF_BY_DEPT&dept=Engineering`
|
|
592
|
+
|
|
593
|
+
## Cross-File Filtering (IN/NOT IN)
|
|
594
|
+
|
|
595
|
+
Use QUERY with NOT IN to find records in one file that don't appear in another. This is the flashcard review pattern — find unreviewed cards by excluding already-reviewed ones. One IN/NOT IN per QUERY.
|
|
596
|
+
|
|
597
|
+
If the IN-file doesn't exist (e.g., new user with no progress), NOT IN treats it as empty — all records pass. This is correct: "kanji not in (nothing)" = all kanji.
|
|
598
|
+
|
|
599
|
+
```chaprola
|
|
600
|
+
// Step 1: Get the list of kanji the user has already reviewed
|
|
601
|
+
QUERY progress INTO reviewed WHERE username EQ PARAM.username
|
|
602
|
+
|
|
603
|
+
// Step 2: Filter flashcards to only those NOT in the reviewed set
|
|
604
|
+
// If progress doesn't exist (new user), all flashcards are returned
|
|
605
|
+
QUERY flashcards INTO new_cards WHERE kanji NOT IN reviewed.kanji
|
|
606
|
+
|
|
607
|
+
// Step 3: Loop through unreviewed cards
|
|
608
|
+
OPEN SECONDARY new_cards
|
|
609
|
+
READ SECONDARY
|
|
610
|
+
IF FINI GOTO empty
|
|
611
|
+
100 PRINT S.kanji + " — " + S.reading + " — " + S.meaning
|
|
612
|
+
READ SECONDARY
|
|
613
|
+
IF FINI GOTO done
|
|
614
|
+
GOTO 100
|
|
615
|
+
empty.
|
|
616
|
+
PRINT "All cards reviewed!"
|
|
617
|
+
done.
|
|
618
|
+
STOP
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
## Public Apps: Use /report, Not /query
|
|
622
|
+
|
|
623
|
+
Site keys require BAA signing. For public-facing web apps, use published reports (`/report`) instead of `/query` for all read operations. `/report` is public — no auth or BAA needed.
|
|
624
|
+
|
|
625
|
+
**Pattern:** Move data logic into Chaprola programs (QUERY, TABULATE), publish them, and call `/report` from the frontend. Reserve site keys for write operations (`/insert-record`) only.
|
|
626
|
+
|
|
627
|
+
```javascript
|
|
628
|
+
// GOOD: public report — no auth needed, works for anyone
|
|
629
|
+
const url = `${API}/report?userid=myapp&project=data&name=RESULTS&poll_id=${id}`;
|
|
630
|
+
const response = await fetch(url);
|
|
631
|
+
|
|
632
|
+
// BAD: /query with site key — fails if BAA not signed (403 Forbidden)
|
|
633
|
+
const response = await fetch(`${API}/query`, {
|
|
634
|
+
headers: { 'Authorization': `Bearer ${SITE_KEY}` },
|
|
635
|
+
body: JSON.stringify({ userid: 'myapp', project: 'data', file: 'votes', where: [...] })
|
|
636
|
+
});
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
**Why this is better:** The program runs server-side with full access. The frontend gets clean output. No API keys exposed for reads. QUERY + TABULATE in a program replaces client-side pivot logic.
|
|
640
|
+
|
|
641
|
+
## Chart Data with TABULATE
|
|
642
|
+
|
|
643
|
+
Use TABULATE to produce CSV output suitable for charting. This example cross-tabulates mortality data by cause and year.
|
|
644
|
+
|
|
645
|
+
```chaprola
|
|
646
|
+
// Generate a pivot table of death counts by cause and year
|
|
647
|
+
TABULATE mortality SUM deaths FOR cause VS year WHERE year GE "2020" INTO trends
|
|
648
|
+
|
|
649
|
+
// Output as CSV — ready for any charting library
|
|
650
|
+
PRINT TABULATE trends AS CSV
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Output:
|
|
654
|
+
```
|
|
655
|
+
cause,2020,2021,2022,2023,total
|
|
656
|
+
Heart disease,690882,693021,699659,702000,2785562
|
|
657
|
+
Cancer,598932,602350,608371,611000,2420653
|
|
658
|
+
...
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
For web apps, use JSON output instead:
|
|
662
|
+
```chaprola
|
|
663
|
+
PRINT TABULATE trends AS JSON
|
|
664
|
+
```
|
|
665
|
+
|
|
558
666
|
### Agent Workflow Summary
|
|
559
667
|
|
|
560
668
|
1. **Inspect** — Call `/format` to see what fields exist
|
package/references/ref-apps.md
CHANGED
|
@@ -37,6 +37,8 @@ Browser → React App → api.chaprola.org (site key in Authorization header)
|
|
|
37
37
|
})
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
**BAA requirement:** All data endpoints (including `/query`) require BAA signing. For public-facing apps that haven't signed a BAA, use published reports (`/report`) for reads instead of `/query`. Move filtering and aggregation into Chaprola programs (QUERY + TABULATE commands), publish them, and call `/report` from the frontend. Reserve the site key for write-only operations like `/insert-record`.
|
|
41
|
+
|
|
40
42
|
**Security model:** The site key is checked against the `Origin` HTTP header, which browsers set automatically. This prevents other websites from using your key (CORS-level protection). However, Origin headers are trivially spoofable from non-browser clients (curl, Postman, scripts). Anyone who extracts the site key from your JavaScript has full access to the account's data. **Use this pattern only for public or semi-public data** — dashboards, product catalogs, published reports. For private data, use the multi-user pattern (each user authenticates individually) or the enterprise proxy pattern.
|
|
41
43
|
|
|
42
44
|
### 2. Multi-User App (each user has their own account)
|
package/references/ref-import.md
CHANGED
|
@@ -44,3 +44,40 @@ Optional `format: "fhir"` for FHIR JSON reconstruction.
|
|
|
44
44
|
## POST /download
|
|
45
45
|
`{userid, project, file, type}` → `{download_url, expires_in, size_bytes}`
|
|
46
46
|
Type: `data`, `format`, `source`, `proc`, `output`.
|
|
47
|
+
|
|
48
|
+
## sort_columns
|
|
49
|
+
|
|
50
|
+
Reorder fields and physically sort data at import time, creating a self-indexing (clustered) data file.
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
POST /import {
|
|
54
|
+
"userid": "...", "project": "...", "name": "STAFF",
|
|
55
|
+
"sort_columns": ["username", "kanji"],
|
|
56
|
+
"data": [...]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- Reorders fields so sort columns come first in the format file
|
|
61
|
+
- Sorts data by those columns at import time
|
|
62
|
+
- Marks `KEY:1`, `KEY:2` in `.F` metadata
|
|
63
|
+
- Enables binary search on the clustered key during QUERY
|
|
64
|
+
|
|
65
|
+
## split_by
|
|
66
|
+
|
|
67
|
+
Split a dataset into per-group data files at import time. One `.DA` file per distinct value of the split field, with a shared `.F` format file.
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
POST /import {
|
|
71
|
+
"userid": "...", "project": "...", "name": "orders",
|
|
72
|
+
"split_by": "region",
|
|
73
|
+
"data": [...]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- Creates one `.DA` per distinct value of the split field
|
|
78
|
+
- Shared `.F` format file
|
|
79
|
+
- Response includes `files_created` and `groups` object
|
|
80
|
+
|
|
81
|
+
## 5GB File Size Limit
|
|
82
|
+
|
|
83
|
+
Maximum 5GB per data file. Returns 413 error if exceeded. Use `split_by` for larger datasets.
|
package/references/ref-pivot.md
CHANGED
|
@@ -44,3 +44,14 @@ For COUNT: `"value": "department", "aggregate": "count"`
|
|
|
44
44
|
SQL equivalent: `SELECT department, year, SUM(revenue) FROM sales GROUP BY department, year`
|
|
45
45
|
|
|
46
46
|
Row and column totals are included automatically in the response.
|
|
47
|
+
|
|
48
|
+
## TABULATE in Programs
|
|
49
|
+
|
|
50
|
+
The `/query` pivot feature is also available in the Chaprola language via the TABULATE command:
|
|
51
|
+
|
|
52
|
+
```chaprola
|
|
53
|
+
TABULATE SALES SUM revenue FOR department VS year WHERE year GE "2020" INTO trends
|
|
54
|
+
PRINT TABULATE trends AS CSV
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
TABULATE produces a matrix in memory — same cross-tabulation as `/query` pivot, but executed inside a program with dynamic PARAM values and chaining with QUERY results.
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# Chaprola Programs (.CS Source)
|
|
2
2
|
|
|
3
3
|
## Compile & Run
|
|
4
|
+
|
|
5
|
+
**Best practice:** Start every program with `OPEN PRIMARY "filename" 0`. The compiler reads the format from the OPEN PRIMARY statement — no `primary_format` parameter needed. This makes programs self-documenting and eliminates compile-time guessing.
|
|
6
|
+
|
|
4
7
|
```bash
|
|
5
|
-
|
|
8
|
+
# Preferred: source declares its own primary file
|
|
9
|
+
POST /compile {userid, project, name: "REPORT", source: "OPEN PRIMARY \"STAFF\" 0\n..."}
|
|
10
|
+
|
|
11
|
+
# Legacy: pass primary_format explicitly (still works, but OPEN PRIMARY is better)
|
|
12
|
+
POST /compile {userid, project, name: "REPORT", source: "...", primary_format: "STAFF"}
|
|
13
|
+
|
|
6
14
|
POST /run {userid, project, name: "REPORT", primary_file: "STAFF", record: 1, async?: true, nophi?: true}
|
|
7
15
|
POST /run/status {userid, project, job_id} # poll async jobs
|
|
8
16
|
POST /publish {userid, project, name, primary_file, acl?: "public|authenticated|owner|token"}
|
|
@@ -111,5 +119,74 @@ LET lvl = PARAM.level // numeric param → R variable
|
|
|
111
119
|
Publish, then call: `POST /report?userid=X&project=Y&name=Z&deck=kanji&level=3`
|
|
112
120
|
Discover params: `POST /report/params {userid, project, name}`
|
|
113
121
|
|
|
122
|
+
## QUERY Command
|
|
123
|
+
|
|
124
|
+
QUERY filters, selects, and reorganizes data inside a Chaprola program — the same power as the `/query` API endpoint, but as a language command.
|
|
125
|
+
|
|
126
|
+
**Output is a .QR file (read-only snapshot).** Cannot be modified with INSERT, UPDATE, or DELETE. Use the original .DA file for writes. R20 is set to the number of matched records.
|
|
127
|
+
|
|
128
|
+
```chaprola
|
|
129
|
+
// Filter + column select
|
|
130
|
+
QUERY STAFF FIELDS name, salary INTO HIGH_PAID WHERE salary GT 80000
|
|
131
|
+
|
|
132
|
+
// Dynamic WHERE with params and R-variables
|
|
133
|
+
QUERY flashcards INTO results WHERE level EQ PARAM.level
|
|
134
|
+
QUERY data INTO subset WHERE score GE R5 AND category EQ PARAM.type
|
|
135
|
+
|
|
136
|
+
// BETWEEN with dynamic bounds
|
|
137
|
+
QUERY data INTO results WHERE age BETWEEN PARAM.min_age PARAM.max_age
|
|
138
|
+
|
|
139
|
+
// Cross-file filtering (IN/NOT IN) — one per QUERY
|
|
140
|
+
QUERY flashcards INTO new_cards WHERE kanji NOT IN progress.kanji
|
|
141
|
+
|
|
142
|
+
// GROUP BY
|
|
143
|
+
QUERY orders INTO summary WHERE year EQ "2026" GROUP BY region COUNT, SUM total ORDER BY SUM_TOTAL DESC LIMIT 5
|
|
144
|
+
|
|
145
|
+
// FROM syntax (alternative to INTO)
|
|
146
|
+
QUERY results FROM STAFF FIELDS name, salary WHERE dept EQ PARAM.dept
|
|
147
|
+
|
|
148
|
+
// OPEN with WHERE (filter directly into file handle)
|
|
149
|
+
OPEN SECONDARY customers WHERE customer_id IN orders.customer_id
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### QUERY Errors
|
|
153
|
+
- **Missing source file:** FOERR flag set, QUERY skipped. Program can check FOERR and branch. (R20 retains its prior value.)
|
|
154
|
+
- **Missing IN-file:** NOT IN treats as empty set (all records pass). IN treats as empty set (no records pass). This is intentional — a new user with no progress file gets all flashcards.
|
|
155
|
+
- **Missing PARAM:** Silently replaced with blank (string) or 0.0 (numeric). Not a hard error — program continues. Check param warnings in the response for diagnostics.
|
|
156
|
+
- **Zero matches:** Not an error. R20 = 0, output .QR is empty.
|
|
157
|
+
|
|
158
|
+
### QUERY Limits
|
|
159
|
+
- One index lookup per QUERY (first EQ condition only)
|
|
160
|
+
- One IN/NOT IN per QUERY
|
|
161
|
+
- No nested QUERY — QUERY is a statement, not an expression
|
|
162
|
+
- Output is always a new file — QUERY never modifies the source
|
|
163
|
+
- FIELDS and GROUP BY are mutually exclusive
|
|
164
|
+
|
|
165
|
+
## TABULATE Command
|
|
166
|
+
|
|
167
|
+
TABULATE produces cross-tabulation matrices inside a program — the language equivalent of `/query` pivot. Result is in-memory only (not written to S3).
|
|
168
|
+
|
|
169
|
+
```chaprola
|
|
170
|
+
TABULATE sales SUM revenue FOR region VS quarter WHERE year EQ "2026" INTO matrix
|
|
171
|
+
PRINT TABULATE matrix AS CSV // CSV output for charting
|
|
172
|
+
PRINT TABULATE matrix AS JSON // JSON matrix for web apps
|
|
173
|
+
PRINT TABULATE matrix AS TABLE // text table for preview
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Aggregates: COUNT, SUM, AVG, MIN, MAX. Multiple aggregates: `TABULATE data COUNT, SUM total FOR row VS col ...`
|
|
177
|
+
|
|
178
|
+
## File Properties
|
|
179
|
+
|
|
180
|
+
```chaprola
|
|
181
|
+
LET R1 = orders.RECORDCOUNT // record count of any loaded file
|
|
182
|
+
IF R1 EQ 0 GOTO no_data
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## INDEX Command
|
|
186
|
+
|
|
187
|
+
```chaprola
|
|
188
|
+
INDEX STAFF ON department // creates STAFF.DEPARTMENT.IDX
|
|
189
|
+
```
|
|
190
|
+
|
|
114
191
|
## Common Field Widths
|
|
115
192
|
ISO datetime: 20, UUID: 36, email: 50, short ID: 8-12, dollar: 10, phone: 15.
|
package/references/ref-query.md
CHANGED
|
@@ -38,3 +38,54 @@ Types: `inner`, `left`, `right`, `full`. Optional `pre_sorted: true` for merge j
|
|
|
38
38
|
- `POST /update-record {userid, project, file, where: [...], set: {field: "value"}}`
|
|
39
39
|
- `POST /delete-record {userid, project, file, where: [...]}`
|
|
40
40
|
- `POST /consolidate {userid, project, file}` — merge .MRG into .DA
|
|
41
|
+
|
|
42
|
+
## QUERY in Programs
|
|
43
|
+
|
|
44
|
+
The QUERY language command does the same thing as the `/query` API but inside a Chaprola program. Use it to filter, select, and reorder data without leaving the runtime.
|
|
45
|
+
|
|
46
|
+
```chaprola
|
|
47
|
+
// In a program, QUERY replaces /query API calls
|
|
48
|
+
QUERY STAFF FIELDS name, salary INTO TOP_EARNERS WHERE salary GT 80000 ORDER BY salary DESC LIMIT 10
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The result is a `.QR` file (read-only snapshot) that can be opened as a secondary file or used in subsequent QUERY commands. R20 is set to the number of matched records. INSERT, UPDATE, and DELETE operations are rejected on .QR files.
|
|
52
|
+
|
|
53
|
+
If the source file doesn't exist, the FOERR flag is set and the QUERY is skipped. If an IN/NOT IN reference file doesn't exist, it's treated as an empty set (NOT IN = all pass, IN = none pass).
|
|
54
|
+
|
|
55
|
+
## Clustered Sort Columns
|
|
56
|
+
|
|
57
|
+
Import with `sort_columns` to create self-indexing files. The data is physically sorted by the key columns at import time, enabling binary search on the clustered key without a separate .IDX file.
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
POST /import {
|
|
61
|
+
"userid": "...", "project": "...", "name": "STAFF",
|
|
62
|
+
"sort_columns": ["department", "name"],
|
|
63
|
+
"data": [...]
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- The .F file marks KEY fields (`KEY:1`, `KEY:2`, etc.)
|
|
68
|
+
- QUERY automatically uses binary search on clustered keys
|
|
69
|
+
- No separate .IDX needed for primary access patterns
|
|
70
|
+
|
|
71
|
+
## split_by on /import
|
|
72
|
+
|
|
73
|
+
Split a dataset into per-group data files at import time. One `.DA` file is created per distinct value of the split field, sharing a single `.F` format file.
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
POST /import {
|
|
77
|
+
"userid": "...", "project": "...", "name": "orders",
|
|
78
|
+
"split_by": "region",
|
|
79
|
+
"data": [...]
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Produces files like `orders/east.DA`, `orders/west.DA`, etc. Access with dynamic filenames in a program:
|
|
84
|
+
|
|
85
|
+
```chaprola
|
|
86
|
+
OPEN PRIMARY orders/PARAM.region
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## BAA and Site Keys
|
|
90
|
+
|
|
91
|
+
The `/query` API endpoint requires BAA signing. Site keys inherit this requirement. For public-facing web apps, use published reports (`/report`) instead of `/query` for all read operations. Move filtering and aggregation logic into Chaprola programs using the QUERY and TABULATE language commands, publish them, and call `/report` from the frontend. Reserve site keys for write-only operations like `/insert-record`.
|