futonic 0.1.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/chunk-5va59f7m.js +20 -0
- package/dist/chunk-6w1vqkpr.js +87 -0
- package/dist/chunk-j2yky04y.js +130 -0
- package/dist/chunk-nj3a3jzm.js +26 -0
- package/dist/chunk-q65vm76v.js +180 -0
- package/dist/cli/generators/drizzle.d.ts +11 -0
- package/dist/cli/generators/drizzle.d.ts.map +1 -0
- package/dist/cli/generators/kysely.d.ts +17 -0
- package/dist/cli/generators/kysely.d.ts.map +1 -0
- package/dist/cli/generators/prisma.d.ts +10 -0
- package/dist/cli/generators/prisma.d.ts.map +1 -0
- package/dist/cli/generators/types.d.ts +24 -0
- package/dist/cli/generators/types.d.ts.map +1 -0
- package/dist/cli/index.d.ts +28 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +101 -0
- package/dist/client/index.d.ts +15 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +7 -0
- package/dist/core/context.d.ts +23 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/host.d.ts +14 -0
- package/dist/core/host.d.ts.map +1 -0
- package/dist/core/service.d.ts +25 -0
- package/dist/core/service.d.ts.map +1 -0
- package/dist/db/internal-adapter.d.ts +43 -0
- package/dist/db/internal-adapter.d.ts.map +1 -0
- package/dist/db/kysely-factory.d.ts +34 -0
- package/dist/db/kysely-factory.d.ts.map +1 -0
- package/dist/db/schema.d.ts +36 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +286 -0
- package/dist/router/endpoint.d.ts +105 -0
- package/dist/router/endpoint.d.ts.map +1 -0
- package/dist/router/middleware.d.ts +10 -0
- package/dist/router/middleware.d.ts.map +1 -0
- package/dist/test-utils.d.ts +25 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
export { __toESM, __require };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import"./chunk-5va59f7m.js";
|
|
2
|
+
|
|
3
|
+
// src/cli/generators/kysely.ts
|
|
4
|
+
var generateKyselySchema = async ({
|
|
5
|
+
tables,
|
|
6
|
+
provider,
|
|
7
|
+
file
|
|
8
|
+
}) => {
|
|
9
|
+
const statements = [];
|
|
10
|
+
for (const [, table] of tables) {
|
|
11
|
+
statements.push(tableToSQL(table, provider));
|
|
12
|
+
}
|
|
13
|
+
const code = statements.join(`
|
|
14
|
+
|
|
15
|
+
`);
|
|
16
|
+
return {
|
|
17
|
+
code: code.trim() || "",
|
|
18
|
+
fileName: file || `./futonic_migrations/${new Date().toISOString().replace(/:/g, "-")}.sql`
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
function tableToSQL(table, provider = "pg") {
|
|
22
|
+
const columns = [];
|
|
23
|
+
for (const [fieldName, field] of Object.entries(table.fields)) {
|
|
24
|
+
columns.push(` ${fieldName} ${fieldToSQLType(field, provider)}`);
|
|
25
|
+
}
|
|
26
|
+
for (const [fieldName, field] of Object.entries(table.fields)) {
|
|
27
|
+
if (field.references) {
|
|
28
|
+
const onDelete = field.references.onDelete ?? "restrict";
|
|
29
|
+
const action = onDelete === "set-null" ? "SET NULL" : onDelete.toUpperCase();
|
|
30
|
+
columns.push(` FOREIGN KEY (${fieldName}) REFERENCES ${field.references.model}(${field.references.field}) ON DELETE ${action}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return `CREATE TABLE IF NOT EXISTS ${table.prefixedName} (
|
|
34
|
+
${columns.join(`,
|
|
35
|
+
`)}
|
|
36
|
+
);`;
|
|
37
|
+
}
|
|
38
|
+
function fieldToSQLType(field, provider) {
|
|
39
|
+
let type;
|
|
40
|
+
switch (field.type) {
|
|
41
|
+
case "string":
|
|
42
|
+
if (provider === "mysql" && (field.unique || field.references)) {
|
|
43
|
+
type = "VARCHAR(255)";
|
|
44
|
+
} else {
|
|
45
|
+
type = "TEXT";
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
case "number":
|
|
49
|
+
type = provider === "mysql" ? "INT" : "INTEGER";
|
|
50
|
+
break;
|
|
51
|
+
case "boolean":
|
|
52
|
+
type = provider === "sqlite" ? "INTEGER" : "BOOLEAN";
|
|
53
|
+
break;
|
|
54
|
+
case "date":
|
|
55
|
+
type = provider === "sqlite" ? "INTEGER" : "TIMESTAMP";
|
|
56
|
+
break;
|
|
57
|
+
case "json":
|
|
58
|
+
if (provider === "pg")
|
|
59
|
+
type = "JSONB";
|
|
60
|
+
else if (provider === "mysql")
|
|
61
|
+
type = "JSON";
|
|
62
|
+
else
|
|
63
|
+
type = "TEXT";
|
|
64
|
+
break;
|
|
65
|
+
default:
|
|
66
|
+
type = "TEXT";
|
|
67
|
+
}
|
|
68
|
+
const parts = [type];
|
|
69
|
+
if (field.primaryKey)
|
|
70
|
+
parts.push("PRIMARY KEY");
|
|
71
|
+
if (field.required)
|
|
72
|
+
parts.push("NOT NULL");
|
|
73
|
+
if (field.unique)
|
|
74
|
+
parts.push("UNIQUE");
|
|
75
|
+
if (field.defaultValue !== undefined) {
|
|
76
|
+
if (typeof field.defaultValue === "string") {
|
|
77
|
+
parts.push(`DEFAULT '${field.defaultValue}'`);
|
|
78
|
+
} else {
|
|
79
|
+
parts.push(`DEFAULT ${field.defaultValue}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return parts.join(" ");
|
|
83
|
+
}
|
|
84
|
+
export {
|
|
85
|
+
tableToSQL,
|
|
86
|
+
generateKyselySchema
|
|
87
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__require,
|
|
3
|
+
__toESM
|
|
4
|
+
} from "./chunk-5va59f7m.js";
|
|
5
|
+
|
|
6
|
+
// src/cli/generators/prisma.ts
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
var generatePrismaSchema = async ({
|
|
11
|
+
tables,
|
|
12
|
+
provider: dbProvider,
|
|
13
|
+
file
|
|
14
|
+
}) => {
|
|
15
|
+
const provider = dbProvider === "pg" ? "postgresql" : dbProvider === "mysql" ? "mysql" : "sqlite";
|
|
16
|
+
const filePath = file || "./prisma/schema.prisma";
|
|
17
|
+
const schemaExists = existsSync(path.resolve(filePath));
|
|
18
|
+
let produceSchema;
|
|
19
|
+
try {
|
|
20
|
+
({ produceSchema } = await import("@mrleebo/prisma-ast"));
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error("@mrleebo/prisma-ast is required for Prisma schema generation. Install it with: npm install @mrleebo/prisma-ast");
|
|
23
|
+
}
|
|
24
|
+
let schemaPrisma = "";
|
|
25
|
+
if (schemaExists) {
|
|
26
|
+
schemaPrisma = await readFile(path.resolve(filePath), "utf-8");
|
|
27
|
+
} else {
|
|
28
|
+
schemaPrisma = getNewPrisma(provider);
|
|
29
|
+
}
|
|
30
|
+
const schema = produceSchema(schemaPrisma, (builder) => {
|
|
31
|
+
for (const [, table] of tables) {
|
|
32
|
+
const modelName = toPascalCase(table.prefixedName);
|
|
33
|
+
const prismaModel = builder.findByType("model", {
|
|
34
|
+
name: modelName
|
|
35
|
+
});
|
|
36
|
+
if (!prismaModel) {
|
|
37
|
+
builder.model(modelName).field("id", "String").attribute("id");
|
|
38
|
+
}
|
|
39
|
+
for (const [fieldName, field] of Object.entries(table.fields)) {
|
|
40
|
+
if (fieldName === "id")
|
|
41
|
+
continue;
|
|
42
|
+
if (prismaModel) {
|
|
43
|
+
const existing = builder.findByType("field", {
|
|
44
|
+
name: fieldName,
|
|
45
|
+
within: prismaModel.properties
|
|
46
|
+
});
|
|
47
|
+
if (existing)
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const prismaType = getPrismaType(field, provider);
|
|
51
|
+
const fieldBuilder = builder.model(modelName).field(fieldName, prismaType);
|
|
52
|
+
if (field.primaryKey) {
|
|
53
|
+
fieldBuilder.attribute("id");
|
|
54
|
+
}
|
|
55
|
+
if (field.unique) {
|
|
56
|
+
builder.model(modelName).blockAttribute(`unique([${fieldName}])`);
|
|
57
|
+
}
|
|
58
|
+
if (field.defaultValue !== undefined && field.defaultValue !== null) {
|
|
59
|
+
if (typeof field.defaultValue === "string") {
|
|
60
|
+
fieldBuilder.attribute(`default("${field.defaultValue}")`);
|
|
61
|
+
} else if (typeof field.defaultValue === "boolean" || typeof field.defaultValue === "number") {
|
|
62
|
+
fieldBuilder.attribute(`default(${field.defaultValue})`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (field.references) {
|
|
66
|
+
const refModelName = toPascalCase(field.references.model);
|
|
67
|
+
let action = "Cascade";
|
|
68
|
+
if (field.references.onDelete === "restrict")
|
|
69
|
+
action = "Restrict";
|
|
70
|
+
else if (field.references.onDelete === "set-null")
|
|
71
|
+
action = "SetNull";
|
|
72
|
+
const relationAttr = `relation(fields: [${fieldName}], references: [${field.references.field}], onDelete: ${action})`;
|
|
73
|
+
builder.model(modelName).field(field.references.model.toLowerCase(), `${refModelName}${field.required ? "" : "?"}`).attribute(relationAttr);
|
|
74
|
+
}
|
|
75
|
+
if (provider === "mysql" && field.type === "string" && !field.unique && !field.references) {
|
|
76
|
+
fieldBuilder.attribute("db.Text");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const hasMapAttr = builder.findByType("attribute", {
|
|
80
|
+
name: "map",
|
|
81
|
+
within: prismaModel?.properties
|
|
82
|
+
});
|
|
83
|
+
if (!hasMapAttr) {
|
|
84
|
+
builder.model(modelName).blockAttribute("map", table.prefixedName);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
const schemaChanged = schema.trim() !== schemaPrisma.trim();
|
|
89
|
+
return {
|
|
90
|
+
code: schemaChanged ? schema : "",
|
|
91
|
+
fileName: filePath,
|
|
92
|
+
overwrite: schemaExists && schemaChanged
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
function getPrismaType(field, provider) {
|
|
96
|
+
const optional = !field.required && !field.primaryKey ? "?" : "";
|
|
97
|
+
switch (field.type) {
|
|
98
|
+
case "string":
|
|
99
|
+
return `String${optional}`;
|
|
100
|
+
case "number":
|
|
101
|
+
return `Int${optional}`;
|
|
102
|
+
case "boolean":
|
|
103
|
+
return `Boolean${optional}`;
|
|
104
|
+
case "date":
|
|
105
|
+
return `DateTime${optional}`;
|
|
106
|
+
case "json":
|
|
107
|
+
if (provider === "sqlite" || provider === "mysql") {
|
|
108
|
+
return `String${optional}`;
|
|
109
|
+
}
|
|
110
|
+
return `Json${optional}`;
|
|
111
|
+
default:
|
|
112
|
+
return `String${optional}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function toPascalCase(str) {
|
|
116
|
+
return str.split("_").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
117
|
+
}
|
|
118
|
+
function getNewPrisma(provider) {
|
|
119
|
+
return `generator client {
|
|
120
|
+
provider = "prisma-client-js"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
datasource db {
|
|
124
|
+
provider = "${provider}"
|
|
125
|
+
url = ${provider === "sqlite" ? `"file:./dev.db"` : `env("DATABASE_URL")`}
|
|
126
|
+
}`;
|
|
127
|
+
}
|
|
128
|
+
export {
|
|
129
|
+
generatePrismaSchema
|
|
130
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/db/schema.ts
|
|
2
|
+
function getServiceTables(services) {
|
|
3
|
+
const result = new Map;
|
|
4
|
+
for (const service of services) {
|
|
5
|
+
if (!service.dbSchema)
|
|
6
|
+
continue;
|
|
7
|
+
for (const [tableName, tableDef] of Object.entries(service.dbSchema.tables)) {
|
|
8
|
+
const prefixedName = `${service.id}_${tableName}`;
|
|
9
|
+
if (result.has(prefixedName)) {
|
|
10
|
+
throw new Error(`Table name collision: "${prefixedName}" is defined by multiple services`);
|
|
11
|
+
}
|
|
12
|
+
result.set(prefixedName, {
|
|
13
|
+
originalName: tableName,
|
|
14
|
+
prefixedName,
|
|
15
|
+
serviceId: service.id,
|
|
16
|
+
fields: tableDef.fields
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
function prefixTableName(serviceId, tableName) {
|
|
23
|
+
return `${serviceId}_${tableName}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { getServiceTables, prefixTableName };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import"./chunk-5va59f7m.js";
|
|
2
|
+
|
|
3
|
+
// src/cli/generators/drizzle.ts
|
|
4
|
+
var generateDrizzleSchema = async ({
|
|
5
|
+
tables,
|
|
6
|
+
provider,
|
|
7
|
+
file
|
|
8
|
+
}) => {
|
|
9
|
+
const filePath = file || "./futonic-schema.ts";
|
|
10
|
+
let code = generateImport({ provider, tables });
|
|
11
|
+
for (const [, table] of tables) {
|
|
12
|
+
code += `
|
|
13
|
+
${generateTable(table, provider)}
|
|
14
|
+
`;
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
code,
|
|
18
|
+
fileName: filePath
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
function generateTable(table, provider) {
|
|
22
|
+
const tableFunc = `${provider}Table`;
|
|
23
|
+
const fields = table.fields;
|
|
24
|
+
const indexes = [];
|
|
25
|
+
const columnDefs = Object.entries(fields).map(([fieldName, field]) => {
|
|
26
|
+
let col = getColumnType(fieldName, field, provider);
|
|
27
|
+
if (field.primaryKey) {
|
|
28
|
+
col += ".primaryKey()";
|
|
29
|
+
}
|
|
30
|
+
if (field.defaultValue !== null && field.defaultValue !== undefined) {
|
|
31
|
+
if (typeof field.defaultValue === "string") {
|
|
32
|
+
col += `.default("${field.defaultValue}")`;
|
|
33
|
+
} else {
|
|
34
|
+
col += `.default(${field.defaultValue})`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (field.required) {
|
|
38
|
+
col += ".notNull()";
|
|
39
|
+
}
|
|
40
|
+
if (field.unique) {
|
|
41
|
+
col += ".unique()";
|
|
42
|
+
indexes.push({
|
|
43
|
+
type: "uniqueIndex",
|
|
44
|
+
name: `${table.prefixedName}_${fieldName}_uidx`,
|
|
45
|
+
on: fieldName
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (field.references) {
|
|
49
|
+
const onDelete = field.references.onDelete || "cascade";
|
|
50
|
+
const action = onDelete === "set-null" ? "set null" : onDelete;
|
|
51
|
+
col += `.references(() => ${field.references.model}.${field.references.field}, { onDelete: '${action}' })`;
|
|
52
|
+
}
|
|
53
|
+
return ` ${fieldName}: ${col}`;
|
|
54
|
+
}).join(`,
|
|
55
|
+
`);
|
|
56
|
+
const indexBlock = assignIndexes(indexes);
|
|
57
|
+
return `export const ${table.prefixedName} = ${tableFunc}("${table.prefixedName}", {
|
|
58
|
+
${columnDefs}
|
|
59
|
+
}${indexBlock});`;
|
|
60
|
+
}
|
|
61
|
+
function getColumnType(name, field, provider) {
|
|
62
|
+
if (field.enum && field.enum.length > 0) {
|
|
63
|
+
const enumValues = field.enum.map((v) => `'${v}'`).join(", ");
|
|
64
|
+
const typeMap2 = {
|
|
65
|
+
sqlite: `text("${name}")`,
|
|
66
|
+
pg: `text("${name}", { enum: [${enumValues}] })`,
|
|
67
|
+
mysql: `mysqlEnum("${name}", [${enumValues}])`
|
|
68
|
+
};
|
|
69
|
+
return typeMap2[provider];
|
|
70
|
+
}
|
|
71
|
+
const typeMap = {
|
|
72
|
+
string: {
|
|
73
|
+
sqlite: `text("${name}")`,
|
|
74
|
+
pg: `text("${name}")`,
|
|
75
|
+
mysql: field.unique ? `varchar("${name}", { length: 255 })` : field.references ? `varchar("${name}", { length: 36 })` : `text("${name}")`
|
|
76
|
+
},
|
|
77
|
+
boolean: {
|
|
78
|
+
sqlite: `integer("${name}", { mode: 'boolean' })`,
|
|
79
|
+
pg: `boolean("${name}")`,
|
|
80
|
+
mysql: `boolean("${name}")`
|
|
81
|
+
},
|
|
82
|
+
number: {
|
|
83
|
+
sqlite: `integer("${name}")`,
|
|
84
|
+
pg: `integer("${name}")`,
|
|
85
|
+
mysql: `int("${name}")`
|
|
86
|
+
},
|
|
87
|
+
date: {
|
|
88
|
+
sqlite: `integer("${name}", { mode: 'timestamp_ms' })`,
|
|
89
|
+
pg: `timestamp("${name}")`,
|
|
90
|
+
mysql: `timestamp("${name}", { fsp: 3 })`
|
|
91
|
+
},
|
|
92
|
+
json: {
|
|
93
|
+
sqlite: `text("${name}", { mode: "json" })`,
|
|
94
|
+
pg: `jsonb("${name}")`,
|
|
95
|
+
mysql: `json("${name}")`
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const dbTypeMap = typeMap[field.type];
|
|
99
|
+
if (!dbTypeMap) {
|
|
100
|
+
throw new Error(`Unsupported field type '${field.type}' for field '${name}'.`);
|
|
101
|
+
}
|
|
102
|
+
return dbTypeMap[provider];
|
|
103
|
+
}
|
|
104
|
+
function assignIndexes(indexes) {
|
|
105
|
+
if (!indexes.length)
|
|
106
|
+
return "";
|
|
107
|
+
const lines = [", (table) => ["];
|
|
108
|
+
for (const index of indexes) {
|
|
109
|
+
lines.push(` ${index.type}("${index.name}").on(table.${index.on}),`);
|
|
110
|
+
}
|
|
111
|
+
lines.push("]");
|
|
112
|
+
return lines.join(`
|
|
113
|
+
`);
|
|
114
|
+
}
|
|
115
|
+
function generateImport({
|
|
116
|
+
provider,
|
|
117
|
+
tables
|
|
118
|
+
}) {
|
|
119
|
+
const coreImports = [`${provider}Table`];
|
|
120
|
+
let hasJson = false;
|
|
121
|
+
let hasDate = false;
|
|
122
|
+
let hasBoolean = false;
|
|
123
|
+
let hasNumber = false;
|
|
124
|
+
let hasEnum = false;
|
|
125
|
+
for (const [, table] of tables) {
|
|
126
|
+
for (const field of Object.values(table.fields)) {
|
|
127
|
+
if (field.type === "json")
|
|
128
|
+
hasJson = true;
|
|
129
|
+
if (field.type === "date")
|
|
130
|
+
hasDate = true;
|
|
131
|
+
if (field.type === "boolean")
|
|
132
|
+
hasBoolean = true;
|
|
133
|
+
if (field.type === "number")
|
|
134
|
+
hasNumber = true;
|
|
135
|
+
if (field.enum && field.enum.length > 0)
|
|
136
|
+
hasEnum = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (provider === "mysql") {
|
|
140
|
+
coreImports.push("varchar", "text");
|
|
141
|
+
} else {
|
|
142
|
+
coreImports.push("text");
|
|
143
|
+
}
|
|
144
|
+
if (hasNumber) {
|
|
145
|
+
if (provider === "mysql")
|
|
146
|
+
coreImports.push("int");
|
|
147
|
+
else
|
|
148
|
+
coreImports.push("integer");
|
|
149
|
+
}
|
|
150
|
+
if (hasBoolean && provider !== "sqlite") {
|
|
151
|
+
coreImports.push("boolean");
|
|
152
|
+
} else if (hasBoolean) {
|
|
153
|
+
if (!hasNumber)
|
|
154
|
+
coreImports.push("integer");
|
|
155
|
+
}
|
|
156
|
+
if (hasDate && provider !== "sqlite") {
|
|
157
|
+
coreImports.push("timestamp");
|
|
158
|
+
} else if (hasDate && !hasNumber && !hasBoolean) {
|
|
159
|
+
coreImports.push("integer");
|
|
160
|
+
}
|
|
161
|
+
if (hasJson) {
|
|
162
|
+
if (provider === "pg")
|
|
163
|
+
coreImports.push("jsonb");
|
|
164
|
+
if (provider === "mysql")
|
|
165
|
+
coreImports.push("json");
|
|
166
|
+
}
|
|
167
|
+
if (hasEnum && provider === "mysql") {
|
|
168
|
+
coreImports.push("mysqlEnum");
|
|
169
|
+
}
|
|
170
|
+
const hasUniqueIdx = [...tables.values()].some((t) => Object.values(t.fields).some((f) => f.unique));
|
|
171
|
+
if (hasUniqueIdx) {
|
|
172
|
+
coreImports.push("uniqueIndex");
|
|
173
|
+
}
|
|
174
|
+
const dedupedImports = [...new Set(coreImports)];
|
|
175
|
+
return `import { ${dedupedImports.join(", ")} } from "drizzle-orm/${provider}-core";
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
export {
|
|
179
|
+
generateDrizzleSchema
|
|
180
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle ORM schema generator.
|
|
3
|
+
*
|
|
4
|
+
* Forked from better-auth `packages/cli/src/generators/drizzle.ts` (MIT).
|
|
5
|
+
* Adapted to read from futonic's PrefixedTable IR instead of getAuthTables().
|
|
6
|
+
* Generates pgTable/mysqlTable/sqliteTable definitions with column types,
|
|
7
|
+
* constraints, references, and indexes.
|
|
8
|
+
*/
|
|
9
|
+
import type { SchemaGenerator } from "./types";
|
|
10
|
+
export declare const generateDrizzleSchema: SchemaGenerator;
|
|
11
|
+
//# sourceMappingURL=drizzle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drizzle.d.ts","sourceRoot":"","sources":["../../../src/cli/generators/drizzle.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAGX,eAAe,EACf,MAAM,SAAS,CAAC;AAEjB,eAAO,MAAM,qBAAqB,EAAE,eAiBnC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw SQL DDL generator for Kysely / plain SQL migrations.
|
|
3
|
+
*
|
|
4
|
+
* better-auth's kysely generator delegates to `getMigrations()` which
|
|
5
|
+
* compiles DDL from their internal schema. We do the same thing here,
|
|
6
|
+
* generating CREATE TABLE statements from our PrefixedTable IR.
|
|
7
|
+
*
|
|
8
|
+
* Forked approach from better-auth `packages/cli/src/generators/kysely.ts` (MIT).
|
|
9
|
+
*/
|
|
10
|
+
import type { PrefixedTable } from "../../db/schema";
|
|
11
|
+
import type { DatabaseProvider, SchemaGenerator } from "./types";
|
|
12
|
+
export declare const generateKyselySchema: SchemaGenerator;
|
|
13
|
+
/**
|
|
14
|
+
* Converts a PrefixedTable to a CREATE TABLE SQL statement.
|
|
15
|
+
*/
|
|
16
|
+
export declare function tableToSQL(table: PrefixedTable, provider?: DatabaseProvider): string;
|
|
17
|
+
//# sourceMappingURL=kysely.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kysely.d.ts","sourceRoot":"","sources":["../../../src/cli/generators/kysely.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAmB,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAEjE,eAAO,MAAM,oBAAoB,EAAE,eAmBlC,CAAC;AAEF;;GAEG;AACH,wBAAgB,UAAU,CACzB,KAAK,EAAE,aAAa,EACpB,QAAQ,GAAE,gBAAuB,GAC/B,MAAM,CAoBR"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prisma schema generator.
|
|
3
|
+
*
|
|
4
|
+
* Forked from better-auth `packages/cli/src/generators/prisma.ts` (MIT).
|
|
5
|
+
* Adapted to read from futonic's PrefixedTable IR instead of getAuthTables().
|
|
6
|
+
* Uses @mrleebo/prisma-ast for AST-based schema patching.
|
|
7
|
+
*/
|
|
8
|
+
import type { SchemaGenerator } from "./types";
|
|
9
|
+
export declare const generatePrismaSchema: SchemaGenerator;
|
|
10
|
+
//# sourceMappingURL=prisma.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["../../../src/cli/generators/prisma.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,eAAO,MAAM,oBAAoB,EAAE,eAyHlC,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generator types for CLI schema generation.
|
|
3
|
+
*
|
|
4
|
+
* Forked from better-auth `packages/cli/src/generators/types.ts` (MIT).
|
|
5
|
+
* Adapted to use futonic's ServiceDBSchema IR instead of BetterAuthOptions.
|
|
6
|
+
*/
|
|
7
|
+
import type { PrefixedTable } from "../../db/schema";
|
|
8
|
+
export interface SchemaGeneratorResult {
|
|
9
|
+
code?: string;
|
|
10
|
+
fileName: string;
|
|
11
|
+
overwrite?: boolean;
|
|
12
|
+
append?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export type DatabaseProvider = "pg" | "mysql" | "sqlite";
|
|
15
|
+
export interface GeneratorOptions {
|
|
16
|
+
/** All prefixed tables collected from mounted services */
|
|
17
|
+
tables: Map<string, PrefixedTable>;
|
|
18
|
+
/** Target database provider */
|
|
19
|
+
provider: DatabaseProvider;
|
|
20
|
+
/** Output file path override */
|
|
21
|
+
file?: string;
|
|
22
|
+
}
|
|
23
|
+
export type SchemaGenerator = (opts: GeneratorOptions) => Promise<SchemaGeneratorResult>;
|
|
24
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/cli/generators/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAChC,0DAA0D;IAC1D,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACnC,+BAA+B;IAC/B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,gCAAgC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,eAAe,GAAG,CAC7B,IAAI,EAAE,gBAAgB,KAClB,OAAO,CAAC,qBAAqB,CAAC,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI factory for service authors.
|
|
3
|
+
*
|
|
4
|
+
* Service packages wrap this to provide their own CLI:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* // packages/acme-billing/src/cli.ts
|
|
8
|
+
* #!/usr/bin/env node
|
|
9
|
+
* import { createCLI } from "futonic/cli";
|
|
10
|
+
* import { billingService } from "./service";
|
|
11
|
+
*
|
|
12
|
+
* createCLI({ service: billingService });
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* The host developer then runs:
|
|
16
|
+
* npx acme-billing generate --orm=drizzle --provider=pg
|
|
17
|
+
*
|
|
18
|
+
* And gets migration files for just that service's tables.
|
|
19
|
+
*/
|
|
20
|
+
import type { EmbeddableService } from "../core/service";
|
|
21
|
+
export interface CLIOptions {
|
|
22
|
+
/** The service definition (pre-mount, has dbSchema) */
|
|
23
|
+
service: EmbeddableService;
|
|
24
|
+
/** Optional name override for CLI output (defaults to service.id) */
|
|
25
|
+
name?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function createCLI(options: CLIOptions): void;
|
|
28
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAIzD,MAAM,WAAW,UAAU;IAC1B,uDAAuD;IACvD,OAAO,EAAE,iBAAiB,CAAC;IAC3B,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAgBnD"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getServiceTables
|
|
3
|
+
} from "../chunk-nj3a3jzm.js";
|
|
4
|
+
import {
|
|
5
|
+
__require,
|
|
6
|
+
__toESM
|
|
7
|
+
} from "../chunk-5va59f7m.js";
|
|
8
|
+
|
|
9
|
+
// src/cli/index.ts
|
|
10
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
function createCLI(options) {
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const command = args[0];
|
|
15
|
+
const serviceName = options.name ?? options.service.id;
|
|
16
|
+
switch (command) {
|
|
17
|
+
case "generate":
|
|
18
|
+
runGenerate(args.slice(1), options).catch((err) => {
|
|
19
|
+
console.error(err);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|
|
22
|
+
break;
|
|
23
|
+
default:
|
|
24
|
+
printUsage(serviceName);
|
|
25
|
+
process.exit(command ? 1 : 0);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function printUsage(name) {
|
|
29
|
+
console.log(`
|
|
30
|
+
${name} CLI (powered by futonic)
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
${name} generate --orm=<drizzle|prisma|kysely> [options]
|
|
34
|
+
|
|
35
|
+
Commands:
|
|
36
|
+
generate Generate database schema files for the host application
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--orm Target ORM (required): drizzle, prisma, kysely
|
|
40
|
+
--provider Database provider: pg, mysql, sqlite (default: pg)
|
|
41
|
+
--out Output file path (optional, defaults vary by ORM)
|
|
42
|
+
`);
|
|
43
|
+
}
|
|
44
|
+
function parseFlag(flags, name) {
|
|
45
|
+
const flag = flags.find((a) => a.startsWith(`--${name}=`));
|
|
46
|
+
return flag?.split("=")[1];
|
|
47
|
+
}
|
|
48
|
+
async function runGenerate(flags, options) {
|
|
49
|
+
const { service } = options;
|
|
50
|
+
const serviceName = options.name ?? service.id;
|
|
51
|
+
const orm = parseFlag(flags, "orm");
|
|
52
|
+
if (!orm) {
|
|
53
|
+
console.error("Error: --orm flag is required");
|
|
54
|
+
console.error(` ${serviceName} generate --orm=drizzle|prisma|kysely`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const provider = parseFlag(flags, "provider") ?? "pg";
|
|
58
|
+
if (!["pg", "mysql", "sqlite"].includes(provider)) {
|
|
59
|
+
console.error(`Invalid provider: ${provider}. Must be pg, mysql, or sqlite`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const outPath = parseFlag(flags, "out");
|
|
63
|
+
const tables = getServiceTables([
|
|
64
|
+
{
|
|
65
|
+
...service,
|
|
66
|
+
mountConfig: { mount: "" }
|
|
67
|
+
}
|
|
68
|
+
]);
|
|
69
|
+
const generator = await loadGenerator(orm);
|
|
70
|
+
const result = await generator({ tables, provider, file: outPath });
|
|
71
|
+
if (!result.code) {
|
|
72
|
+
console.log(`[${serviceName}] No schema changes detected.`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const dir = path.dirname(result.fileName);
|
|
76
|
+
await mkdir(dir, { recursive: true });
|
|
77
|
+
await writeFile(result.fileName, result.code, "utf-8");
|
|
78
|
+
console.log(`[${serviceName}] Generated ${orm} schema → ${result.fileName}`);
|
|
79
|
+
}
|
|
80
|
+
async function loadGenerator(orm) {
|
|
81
|
+
switch (orm) {
|
|
82
|
+
case "drizzle": {
|
|
83
|
+
const { generateDrizzleSchema } = await import("../chunk-q65vm76v.js");
|
|
84
|
+
return generateDrizzleSchema;
|
|
85
|
+
}
|
|
86
|
+
case "prisma": {
|
|
87
|
+
const { generatePrismaSchema } = await import("../chunk-j2yky04y.js");
|
|
88
|
+
return generatePrismaSchema;
|
|
89
|
+
}
|
|
90
|
+
case "kysely": {
|
|
91
|
+
const { generateKyselySchema } = await import("../chunk-6w1vqkpr.js");
|
|
92
|
+
return generateKyselySchema;
|
|
93
|
+
}
|
|
94
|
+
default:
|
|
95
|
+
console.error(`Unknown ORM: ${orm}. Supported: drizzle, prisma, kysely`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
createCLI
|
|
101
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports better-call's client utilities for type-safe API consumption.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { createClient } from "futonic/client";
|
|
7
|
+
* import type { billingRouter } from "@acme/billing";
|
|
8
|
+
*
|
|
9
|
+
* const client = createClient<typeof billingRouter>({
|
|
10
|
+
* baseURL: "/api/billing",
|
|
11
|
+
* });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export { createClient } from "better-call/client";
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
|