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.
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/emit_admin.js +2 -1
- package/dist/emit_admin.js.map +1 -1
- package/dist/emit_audit.js +2 -2
- package/dist/emit_audit.js.map +1 -1
- package/dist/emit_cron.d.ts +6 -0
- package/dist/emit_cron.js +66 -0
- package/dist/emit_cron.js.map +1 -0
- package/dist/emit_full.d.ts +6 -1
- package/dist/emit_full.js +28 -5
- package/dist/emit_full.js.map +1 -1
- package/dist/emit_graphql.d.ts +6 -0
- package/dist/emit_graphql.js +140 -0
- package/dist/emit_graphql.js.map +1 -0
- package/dist/emit_notify.d.ts +6 -0
- package/dist/emit_notify.js +85 -0
- package/dist/emit_notify.js.map +1 -0
- package/dist/emit_runtime.js +5 -0
- package/dist/emit_runtime.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/scaffold.js +3 -1
- package/dist/scaffold.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +2 -2
- package/src/emit_admin.ts +2 -1
- package/src/emit_audit.ts +2 -2
- package/src/emit_cron.ts +70 -0
- package/src/emit_full.ts +36 -5
- package/src/emit_graphql.ts +161 -0
- package/src/emit_notify.ts +88 -0
- package/src/emit_runtime.ts +5 -0
- package/src/index.ts +3 -0
- package/src/scaffold.ts +3 -1
|
@@ -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
|
+
}
|
package/src/emit_runtime.ts
CHANGED
|
@@ -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:
|
|
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 {
|