bonescript-compiler 0.5.7 → 0.5.8

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.
@@ -0,0 +1,161 @@
1
+ /**
2
+ * BoneScript GraphQL Schema Emitter
3
+ * Generates schema.graphql from an IRSystem.
4
+ */
5
+
6
+ import * as IR from "./ir";
7
+
8
+ function toCamelCase(s: string): string {
9
+ return s.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
10
+ }
11
+
12
+ function toCamelCasePlural(s: string): string {
13
+ const camel = toCamelCase(s);
14
+ // Simple pluralisation: append 's' (good enough for generated stubs)
15
+ return camel.endsWith("s") ? camel : camel + "s";
16
+ }
17
+
18
+ function toGraphQLType(irType: string): string {
19
+ // Handle optional<X> — nullable in GraphQL (just unwrap, no !)
20
+ const optMatch = irType.match(/^optional<(.+)>$/);
21
+ if (optMatch) return toGraphQLType(optMatch[1]);
22
+
23
+ // Handle list<X> / set<X>
24
+ const listMatch = irType.match(/^(list|set)<(.+)>$/);
25
+ if (listMatch) return `[${toGraphQLType(listMatch[2])}]`;
26
+
27
+ switch (irType) {
28
+ case "string": return "String";
29
+ case "uint":
30
+ case "int": return "Int";
31
+ case "float": return "Float";
32
+ case "bool": return "Boolean";
33
+ case "timestamp": return "String";
34
+ case "uuid": return "ID";
35
+ case "bytes": return "String";
36
+ case "json": return "String";
37
+ default: return "String";
38
+ }
39
+ }
40
+
41
+ function isOptional(irType: string): boolean {
42
+ return irType.startsWith("optional<");
43
+ }
44
+
45
+ function fieldLine(field: IR.IRField): string {
46
+ const gqlType = toGraphQLType(field.type);
47
+ const nullable = isOptional(field.type) || field.nullable;
48
+ return ` ${field.name}: ${gqlType}${nullable ? "" : "!"}`;
49
+ }
50
+
51
+ export function emitGraphQLSchema(system: IR.IRSystem): string {
52
+ const lines: string[] = [];
53
+
54
+ lines.push(`# Generated by BoneScript compiler. DO NOT EDIT.`);
55
+ lines.push(`# System: ${system.name} v${system.version}`);
56
+ lines.push(``);
57
+
58
+ // Collect all models from api_service modules
59
+ const apiModules = system.modules.filter(m => m.kind === "api_service" && m.models.length > 0);
60
+
61
+ const queryFields: string[] = [];
62
+ const mutationFields: string[] = [];
63
+ const extraTypes: string[] = [];
64
+
65
+ for (const mod of apiModules) {
66
+ const model = mod.models[0];
67
+ const modelName = model.name;
68
+ const camelName = toCamelCase(modelName.charAt(0).toLowerCase() + modelName.slice(1));
69
+ const camelPlural = toCamelCasePlural(modelName.charAt(0).toLowerCase() + modelName.slice(1));
70
+
71
+ // ─── type {ModelName} ───
72
+ lines.push(`type ${modelName} {`);
73
+ // Always include id, created_at, updated_at
74
+ lines.push(` id: ID!`);
75
+ for (const field of model.fields) {
76
+ if (field.name === "id") continue; // already emitted
77
+ lines.push(fieldLine(field));
78
+ }
79
+ lines.push(`}`);
80
+ lines.push(``);
81
+
82
+ // ─── type {ModelName}Page ───
83
+ lines.push(`type ${modelName}Page {`);
84
+ lines.push(` items: [${modelName}!]!`);
85
+ lines.push(` total: Int!`);
86
+ lines.push(` page: Int!`);
87
+ lines.push(` pageSize: Int!`);
88
+ lines.push(`}`);
89
+ lines.push(``);
90
+
91
+ // ─── input {ModelName}Input ───
92
+ const inputFields = model.fields.filter(
93
+ f => !["id", "created_at", "updated_at"].includes(f.name)
94
+ );
95
+ lines.push(`input ${modelName}Input {`);
96
+ for (const field of inputFields) {
97
+ lines.push(fieldLine(field));
98
+ }
99
+ lines.push(`}`);
100
+ lines.push(``);
101
+
102
+ // Capability inputs + results
103
+ const capabilities = mod.interfaces.flatMap(i => i.methods)
104
+ .filter(m => !["create", "read", "update", "delete", "list"].includes(m.name));
105
+
106
+ for (const cap of capabilities) {
107
+ const capPascal = cap.name.charAt(0).toUpperCase() + toCamelCase(cap.name).slice(1);
108
+ extraTypes.push(`input ${modelName}${capPascal}Input {`);
109
+ for (const param of cap.input) {
110
+ extraTypes.push(fieldLine(param));
111
+ }
112
+ extraTypes.push(`}`);
113
+ extraTypes.push(``);
114
+ }
115
+
116
+ // ─── Query fields ───
117
+ queryFields.push(` ${camelName}(id: ID!): ${modelName}`);
118
+ queryFields.push(` ${camelPlural}(page: Int, pageSize: Int): ${modelName}Page`);
119
+
120
+ // ─── Mutation fields ───
121
+ mutationFields.push(` create${modelName}(input: ${modelName}Input!): ${modelName}!`);
122
+ mutationFields.push(` update${modelName}(id: ID!, input: ${modelName}Input!): ${modelName}!`);
123
+ mutationFields.push(` delete${modelName}(id: ID!): Boolean!`);
124
+
125
+ for (const cap of capabilities) {
126
+ const capCamel = toCamelCase(cap.name);
127
+ const capPascal = cap.name.charAt(0).toUpperCase() + capCamel.slice(1);
128
+ mutationFields.push(` ${capCamel}(input: ${modelName}${capPascal}Input): CapabilityResult!`);
129
+ }
130
+ }
131
+
132
+ // ─── type CapabilityResult ───
133
+ lines.push(`type CapabilityResult {`);
134
+ lines.push(` ok: Boolean!`);
135
+ lines.push(` action: String!`);
136
+ lines.push(`}`);
137
+ lines.push(``);
138
+
139
+ // ─── Extra capability input types ───
140
+ for (const l of extraTypes) {
141
+ lines.push(l);
142
+ }
143
+
144
+ // ─── type Query ───
145
+ lines.push(`type Query {`);
146
+ for (const f of queryFields) {
147
+ lines.push(f);
148
+ }
149
+ lines.push(`}`);
150
+ lines.push(``);
151
+
152
+ // ─── type Mutation ───
153
+ lines.push(`type Mutation {`);
154
+ for (const f of mutationFields) {
155
+ lines.push(f);
156
+ }
157
+ lines.push(`}`);
158
+ lines.push(``);
159
+
160
+ return lines.join("\n");
161
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * BoneScript Notification Service Emitter
3
+ * Generates src/notify.ts — fires on event emissions.
4
+ */
5
+
6
+ import * as IR from "./ir";
7
+
8
+ function toPascalCase(s: string): string {
9
+ return s.replace(/(^|[-_\s])(\w)/g, (_, __, c: string) => c.toUpperCase());
10
+ }
11
+
12
+ export function emitNotifyService(system: IR.IRSystem): string {
13
+ const lines: string[] = [];
14
+
15
+ lines.push(`// Generated by BoneScript compiler.`);
16
+ lines.push(`// Notification service — fires on event emissions.`);
17
+ lines.push(`// Configure NOTIFY_PROVIDER=resend|sendgrid|log (default: log)`);
18
+ lines.push(`// Set NOTIFY_API_KEY and NOTIFY_FROM_EMAIL in .env`);
19
+ lines.push(``);
20
+ lines.push(`import { SystemEvent } from "./events";`);
21
+ lines.push(``);
22
+ lines.push(`export type NotifyProvider = "resend" | "sendgrid" | "log";`);
23
+ lines.push(``);
24
+ lines.push(`const PROVIDER = (process.env.NOTIFY_PROVIDER || "log") as NotifyProvider;`);
25
+ lines.push(`const API_KEY = process.env.NOTIFY_API_KEY || "";`);
26
+ lines.push(`const FROM_EMAIL = process.env.NOTIFY_FROM_EMAIL || "noreply@example.com";`);
27
+ lines.push(``);
28
+ lines.push(`export interface NotifyMessage {`);
29
+ lines.push(` to: string;`);
30
+ lines.push(` subject: string;`);
31
+ lines.push(` body: string;`);
32
+ lines.push(`}`);
33
+ lines.push(``);
34
+ lines.push(`async function sendEmail(msg: NotifyMessage): Promise<void> {`);
35
+ lines.push(` if (PROVIDER === "log") {`);
36
+ lines.push(` console.log(\`[notify] \${msg.subject} → \${msg.to}\`);`);
37
+ lines.push(` return;`);
38
+ lines.push(` }`);
39
+ lines.push(` if (PROVIDER === "resend") {`);
40
+ lines.push(` const res = await fetch("https://api.resend.com/emails", {`);
41
+ lines.push(` method: "POST",`);
42
+ lines.push(` headers: { "Authorization": \`Bearer \${API_KEY}\`, "Content-Type": "application/json" },`);
43
+ lines.push(` body: JSON.stringify({ from: FROM_EMAIL, to: msg.to, subject: msg.subject, html: msg.body }),`);
44
+ lines.push(` });`);
45
+ lines.push(` if (!res.ok) throw new Error(\`Resend error: \${res.status}\`);`);
46
+ lines.push(` return;`);
47
+ lines.push(` }`);
48
+ lines.push(` if (PROVIDER === "sendgrid") {`);
49
+ lines.push(` const res = await fetch("https://api.sendgrid.com/v3/mail/send", {`);
50
+ lines.push(` method: "POST",`);
51
+ lines.push(` headers: { "Authorization": \`Bearer \${API_KEY}\`, "Content-Type": "application/json" },`);
52
+ lines.push(` body: JSON.stringify({ personalizations: [{ to: [{ email: msg.to }] }], from: { email: FROM_EMAIL }, subject: msg.subject, content: [{ type: "text/html", value: msg.body }] }),`);
53
+ lines.push(` });`);
54
+ lines.push(` if (!res.ok) throw new Error(\`SendGrid error: \${res.status}\`);`);
55
+ lines.push(` return;`);
56
+ lines.push(` }`);
57
+ lines.push(`}`);
58
+ lines.push(``);
59
+
60
+ // Per-event handlers
61
+ for (const event of system.events) {
62
+ const eventName = toPascalCase(event.name);
63
+ lines.push(`// Handler for ${eventName}`);
64
+ lines.push(`export async function notify${eventName}(event: SystemEvent, recipientEmail: string): Promise<void> {`);
65
+ lines.push(` await sendEmail({`);
66
+ lines.push(` to: recipientEmail,`);
67
+ lines.push(` subject: "${eventName} notification",`);
68
+ lines.push(` body: \`<p>Event <strong>${eventName}</strong> occurred.</p><pre>\${JSON.stringify(event.payload, null, 2)}</pre>\`,`);
69
+ lines.push(` });`);
70
+ lines.push(`}`);
71
+ lines.push(``);
72
+ }
73
+
74
+ // registerNotificationHandlers
75
+ lines.push(`export function registerNotificationHandlers(eventBus: any): void {`);
76
+ lines.push(` // TODO: implement recipient lookup per event type`);
77
+ for (const event of system.events) {
78
+ const eventName = toPascalCase(event.name);
79
+ lines.push(` // eventBus.subscribe("${eventName}", async (event: SystemEvent) => {`);
80
+ lines.push(` // const recipientEmail = await lookupRecipient(event);`);
81
+ lines.push(` // await notify${eventName}(event, recipientEmail);`);
82
+ lines.push(` // });`);
83
+ }
84
+ lines.push(`}`);
85
+ lines.push(``);
86
+
87
+ return lines.join("\n");
88
+ }
@@ -57,6 +57,7 @@ export function emitPackageJson(system: IR.IRSystem): string {
57
57
  "express-rate-limit": "7.1.5",
58
58
  jsonwebtoken: "9.0.2",
59
59
  dotenv: "16.3.1",
60
+ "node-cron": "3.0.3",
60
61
  },
61
62
  devDependencies: {
62
63
  "@types/express": "4.17.21",
@@ -66,6 +67,7 @@ export function emitPackageJson(system: IR.IRSystem): string {
66
67
  "@types/cors": "2.8.17",
67
68
  "@types/jsonwebtoken": "9.0.5",
68
69
  "@types/uuid": "9.0.7",
70
+ "@types/node-cron": "3.0.11",
69
71
  typescript: "5.3.3",
70
72
  "ts-node": "10.9.2",
71
73
  },
@@ -576,6 +578,7 @@ export function emitIndex(system: IR.IRSystem): string {
576
578
  );
577
579
  if (hasBatch) {
578
580
  lines.push(`import { startBatchWorker } from "./batch";`);
581
+ lines.push(`import { startCronJobs } from "./cron";`);
579
582
  }
580
583
  if (hasWebSocket) {
581
584
  lines.push(`import { setupWebSocketServer } from "./websocket";`);
@@ -653,6 +656,8 @@ export function emitIndex(system: IR.IRSystem): string {
653
656
  if (hasBatch) {
654
657
  lines.push(` // Start batch executor`);
655
658
  lines.push(` startBatchWorker();`);
659
+ lines.push(` // Start cron jobs`);
660
+ lines.push(` startCronJobs();`);
656
661
  }
657
662
  lines.push(` console.log(\`[${system.name}] Running on port \${PORT}\`);`);
658
663
  lines.push(` console.log(\` HTTP routes:\`);`);
package/src/index.ts CHANGED
@@ -48,6 +48,9 @@ export { emitPostmanCollection } from "./emit_postman";
48
48
  export { emitSeedFile } from "./emit_seed";
49
49
  export { emitAuditSchema, emitAuditMiddleware } from "./emit_audit";
50
50
  export { emitAdminPanel } from "./emit_admin";
51
+ export { emitNotifyService } from "./emit_notify";
52
+ export { emitCronJobs } from "./emit_cron";
53
+ export { emitGraphQLSchema } from "./emit_graphql";
51
54
 
52
55
  /**
53
56
  * Convenience function: compile a .bone source string to files.
package/src/scaffold.ts CHANGED
@@ -15,7 +15,9 @@ export type ScaffoldDomain =
15
15
  | "realtime_collaboration";
16
16
 
17
17
  const TEMPLATES: Record<ScaffoldDomain, string> = {
18
- multiplayer_game: `system MyGame {
18
+ multiplayer_game: `// Compile with: bonec compile {name}.bone --target nakama
19
+ // for Nakama TypeScript runtime output instead of Express/PostgreSQL
20
+ system MyGame {
19
21
  domain: multiplayer_game
20
22
 
21
23
  entity Player {