This commit is contained in:
fenris 2026-03-06 08:41:27 +01:00
commit bfed93cf9e
29 changed files with 11151 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
temp/
build/
.geany

60
doc/brock.schema.json Normal file
View file

@ -0,0 +1,60 @@
{
"type": "object",
"properties": {
"database": {
"type": "object",
"properties": {
"kind": {
"type": "string"
},
"data": {
"type": "object",
"properties": {
},
"additionalProperties": {
},
"required": [
]
}
},
"additionalProperties": false,
"required": [
"kind"
]
},
"backend": {
"type": "object",
"properties": {
"schema": {
"type": "string",
"enum": ["http", "https"]
},
"host": {
"type": "string"
},
"port": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"schema",
"host",
"port"
]
},
"frontend": {
"type": "object",
"properties": {
},
"additionalProperties": false,
"required": [
]
}
},
"required": [
"database",
"backend",
"frontend"
]
}

View file

@ -0,0 +1,66 @@
{
"domains": [
{
"name": "address",
"description": "collection of addresses",
"key_field": {
"name": "id"
},
"data_fields": [
{
"name": "city",
"description": "the name of the city",
"nullable": false,
"type": "string_medium"
},
{
"name": "zip",
"description": "the postal code",
"nullable": false,
"type": "string_medium"
},
{
"name": "street",
"description": "the name of the street and the house number",
"nullable": false,
"type": "string_medium"
}
]
},
{
"name": "person",
"description": "collection of contacts",
"key_field": {
"name": "id"
},
"data_fields": [
{
"name": "prename",
"description": "first name of the person",
"nullable": false,
"type": "string_medium"
},
{
"name": "surname",
"description": "last name of the person",
"nullable": false,
"type": "string_medium"
},
{
"name": "address_id",
"description": "reference to the associated address dataset",
"nullable": false,
"type": "integer"
},
{
"name": "email_address",
"description": "optional eMail address",
"nullable": true,
"type": "string_medium",
"default": null
}
]
}
]
}

View file

@ -0,0 +1,13 @@
{
"domains": [
{
"name": "sweets",
"description": "collection of sweets",
"key_field": {"name": "id"},
"data_fields": [
{"name": "name", "type": "string_medium"},
{"name": "calories", "type": "integer"}
]
}
]
}

129
doc/sindri.schema.json Normal file
View file

@ -0,0 +1,129 @@
{
"type": "object",
"additionalProperties": false,
"properties": {
"domains": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"description": {
"type": [
"null",
"string"
],
"default": null
},
"key_field": {
"type": [
"null",
"object"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"description": {
"type": [
"null",
"string"
],
"default": null
}
},
"required": [
"name"
],
"default": null
},
"data_fields": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"description": {
"type": [
"null",
"string"
],
"default": null
},
"type": {
"type": "string",
"enum": [
"boolean",
"integer",
"float",
"string_short",
"string_medium",
"string_long"
]
},
"nullable": {
"type": "boolean",
"default": true
},
"default": {
"type": [
"null",
"boolean",
"integer",
"float",
"string"
],
"default": null
}
},
"required": [
"name",
"type"
]
},
"default": []
},
"constraints": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"kind": {
"type": "string",
"enum": [
"unique",
"foreign_key"
]
},
"parameters": {
"type": "object",
"additionalProperties": "string",
"properties": {},
"required": []
}
},
"required": [
"kind"
]
},
"default": []
}
},
"required": [
"name"
]
}
}
},
"required": [
"domains"
]
}

1880
lib/plankton/plankton.d.ts vendored Normal file

File diff suppressed because it is too large Load diff

6668
lib/plankton/plankton.js Normal file

File diff suppressed because it is too large Load diff

94
readme.md Normal file
View file

@ -0,0 +1,94 @@
# sindri
erstellt Datenmodell-Skripte in verschiedenen Ausgabe-Sprachen (MySQL, SQLite, …) auf Basis einer abstrakten Beschreibung
## Erstellung
### Voraussetzungen
- Typescript-Compiler
- GNU Make
### Anweisungen
- `tools/build` ausführen
## Dokumentation
Beispiel-Nutzung:
```sh
tools/build
cd build
cat ../doc/examples/contacts.sindri.json | ./sindri -f database:sqlite
```
… erzeugt:
```sql
CREATE TABLE
`address`(
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`city` VARCHAR(255) NOT NULL,
`zip` VARCHAR(255) NOT NULL,
`street` VARCHAR(255) NOT NULL
)
;
CREATE TABLE
`person`(
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`prename` VARCHAR(255) NOT NULL,
`surname` VARCHAR(255) NOT NULL,
`address_id` INTEGER NOT NULL,
`email_address` VARCHAR(255) DEFAULT NULL
)
;
```
Der Befehl muss nur minimal gewändert werden um die PostgreSQL-Ausgabe zu erhalten:
```sh
cat ../doc/examples/contacts.sindri.json | ./sindri -f database:postgresql
```
… erzeugt:
```sql
CREATE TABLE
address(
"id" SERIAL,
"city" VARCHAR(255) NOT NULL,
"zip" VARCHAR(255) NOT NULL,
"street" VARCHAR(255) NOT NULL,
UNIQUE ("id")
)
;
COMMENT ON TABLE address IS 'collection of addresses';
COMMENT ON COLUMN address.city IS 'the name of the city';
COMMENT ON COLUMN address.zip IS 'the postal code';
COMMENT ON COLUMN address.street IS 'the name of the street and the house number';
CREATE TABLE
person(
"id" SERIAL,
"prename" VARCHAR(255) NOT NULL,
"surname" VARCHAR(255) NOT NULL,
"address_id" INTEGER NOT NULL,
"email_address" VARCHAR(255) DEFAULT NULL,
UNIQUE ("id")
)
;
COMMENT ON TABLE person IS 'collection of contacts';
COMMENT ON COLUMN person.prename IS 'first name of the person';
COMMENT ON COLUMN person.surname IS 'last name of the person';
COMMENT ON COLUMN person.address_id IS 'reference to the associated address dataset';
COMMENT ON COLUMN person.email_address IS 'optional eMail address';
```

127
source/base.ts Normal file
View file

@ -0,0 +1,127 @@
declare var __dirname;
namespace _sindri
{
/**
*/
export type type_input = {
domains : Array<
{
name : string;
description : (null | string);
key_field : (
null
|
{
name : string;
description ?: (null | string);
}
);
data_fields : Array<
{
name : string;
description : (null | string);
type : ("boolean" | "integer" | "float" | "string_short" | "string_medium" | "string_long");
nullable : boolean;
default : (null | boolean | int | float | string);
}
>;
constraints ?: Array<
{
kind : ("unique" | "foreign_key");
parameters : Record<string, any>;
}
>;
}
>;
};
/**
*/
export type type_output = {
render : ((input_data : type_input) => Promise<string>);
};
/**
*/
export enum enum_realm {
database = "database",
backend = "backend",
frontend = "frontend",
other = "other",
}
/**
*/
var _outputs : Record<enum_realm, Record<string, _sindri.type_output>> = {
[enum_realm.database]: {},
[enum_realm.backend]: {},
[enum_realm.frontend]: {},
[enum_realm.other]: {},
};
/**
*/
export function add_output(
realm : enum_realm,
implementation : string,
output : _sindri.type_output
) : void
{
_outputs[realm][implementation] = output;
}
/**
*/
export function get_output(
realm : enum_realm,
implementation : string
) : _sindri.type_output
{
return _outputs[realm][implementation];
}
/**
*/
export function list_outputs(
) : Array<{realm : enum_realm; implementation : string;}>
{
return (
Object.entries(_outputs)
.map(
([realm, group]) => (
Object.keys(group)
.map(
implementation => ({"realm": (realm as enum_realm), "implementation": implementation})
)
)
)
.reduce(
(x, y) => x.concat(y),
[]
)
);
}
/**
*/
export function get_template(
realm : enum_realm,
implementation : string,
name : string
) : Promise<string>
{
return lib_plankton.file.read(
[__dirname, "templates", realm, implementation, name].join("/")
);
}
}

189
source/conf.ts Normal file
View file

@ -0,0 +1,189 @@
namespace _sindri
{
/**
* @todo generate generic
*/
export function input_schema(
) : any
{
return {
"type": "object",
"additionalProperties": false,
"properties": {
"domains": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"description": {
"type": ["null", "string"],
"default": null
},
"key_field": {
"type": ["null","object"],
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"description": {
"type": ["null", "string"],
"default": null
}
},
"required": [
"name"
],
"default": null
},
"data_fields": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"description": {
"type": ["null", "string"],
"default": null
},
"type": {
"type": "string",
"enum": [
"boolean",
"integer",
"float",
"string_short",
"string_medium",
"string_long"
]
},
"nullable": {
"type": "boolean",
"default": true
},
"default": {
"type": ["null", "boolean", "integer", "float", "string"],
"default": null
}
},
"required": [
"name",
"type"
]
},
"default": []
},
"constraints": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"kind": {
"type": "string",
"enum": [
"unique",
"foreign_key"
]
},
"parameters": {
"type": "object",
"additionalProperties": "string",
"properties": {
},
"required": [
]
}
},
"required": [
"kind"
]
},
"default": []
}
},
"required": [
"name"
]
}
}
},
"required": [
"domains"
]
}
}
/**
*/
export function input_normalize(
input_raw : any
) : type_input
{
// validate
if (! input_raw.hasOwnProperty("domains")) {
throw (new Error("input node is missing mandatory field 'domains'"));
}
else {
// sanitize
return {
"domains": (
input_raw["domains"]
.map(
domain_raw => ({
"name": domain_raw["name"],
"description": (domain_raw["description"] ?? null),
"key_field": (
(domain_raw.hasOwnProperty("key_field") && (domain_raw["key_field"] !== null))
? {
"name": domain_raw["key_field"]["name"],
"description": (domain_raw["key_field"]["description"] ?? null),
}
: null
),
"data_fields": (
(domain_raw.hasOwnProperty("data_fields") && (domain_raw["data_fields"] !== null))
? (
domain_raw["data_fields"]
.map(
data_field_raw => ({
"name": data_field_raw["name"],
"description": (data_field_raw["description"] ?? null),
"type": data_field_raw["type"],
"nullable": (data_field_raw["nullable"] ?? true),
"default": data_field_raw["default"],
})
)
)
: []
),
"constraints": (
(domain_raw.hasOwnProperty("constraints") && (domain_raw["constraints"] !== null))
? (
domain_raw["constraints"]
.map(
constraint_raw => ({
"kind": constraint_raw["kind"],
"parameters": (constraint_raw["parameters"] ?? {}),
})
)
)
: []
),
})
)
),
};
}
}
}

135
source/main.ts Normal file
View file

@ -0,0 +1,135 @@
namespace _sindri
{
/**
*/
export async function main(
args_raw : Array<string>
) : Promise<void>
{
const arg_handler = new lib_plankton.args.class_handler(
{
"format": new lib_plankton.args.class_argument({
"name": "format",
"type": lib_plankton.args.enum_type.string,
"kind": lib_plankton.args.enum_kind.volatile,
"mode": lib_plankton.args.enum_mode.replace,
"default": "database:sqlite",
"parameters": {
"indicators_long": ["format"],
"indicators_short": ["f"],
},
"info": "output format",
}),
"list": new lib_plankton.args.class_argument({
"name": "list",
"type": lib_plankton.args.enum_type.boolean,
"kind": lib_plankton.args.enum_kind.volatile,
"mode": lib_plankton.args.enum_mode.replace,
"default": false,
"parameters": {
"indicators_long": ["list"],
"indicators_short": ["l"],
},
"info": "list available output formats",
}),
"schema": new lib_plankton.args.class_argument({
"name": "schema",
"type": lib_plankton.args.enum_type.boolean,
"kind": lib_plankton.args.enum_kind.volatile,
"mode": lib_plankton.args.enum_mode.replace,
"default": false,
"parameters": {
"indicators_long": ["schema"],
"indicators_short": ["s"],
},
"info": "print sindri JSON schema to stdout and exit",
}),
"help": new lib_plankton.args.class_argument({
"name": "help",
"type": lib_plankton.args.enum_type.boolean,
"kind": lib_plankton.args.enum_kind.volatile,
"mode": lib_plankton.args.enum_mode.replace,
"default": false,
"parameters": {
"indicators_long": ["help"],
"indicators_short": ["h"],
},
"info": "print help to stdout and exit",
}),
}
);
const args : Record<string, any> = arg_handler.read(lib_plankton.args.enum_environment.cli, args_raw.join(" "));
if (args["help"]) {
process.stdout.write(
arg_handler.generate_help(
{
"programname": "sindri",
"author": "Christian Fraß <frass@greenscale.de>",
"description": "create data model scripts in different output formats (MySQL, SQLite, …) on basis of an abstract description; feed with .sindri.json file via stdin!",
"executable": "sindri",
}
)
);
}
else {
if (args["schema"]) {
process.stdout.write(
JSON.stringify(_sindri.input_schema(), undefined, "\t")
);
}
else {
if (args["list"]) {
process.stdout.write(
_sindri.list_outputs()
.map(
entry => lib_plankton.string.coin(
"{{realm}}:{{implementation}}\n",
{
"realm": entry.realm,
"implementation": entry.implementation,
}
)
)
.join("")
);
}
else {
const input_content : string = await lib_plankton.file.read_stdin();
const input_data_raw : any = lib_plankton.json.decode(input_content);
const input_data : type_input = _sindri.input_normalize(input_data_raw);
const format_parts : Array<string> = args["format"].split(":");
const realm_encoded : string = format_parts[0];
const realm : _sindri.enum_realm = {
"database": _sindri.enum_realm.database,
"backend": _sindri.enum_realm.backend,
"frontend": _sindri.enum_realm.frontend,
"other": _sindri.enum_realm.other,
}[realm_encoded];
const name : string = format_parts.slice(1).join(":");
let output : (null | _sindri.type_output);
try {
output = _sindri.get_output(realm, name);
}
catch (error) {
output = null;
}
if (output === null) {
throw (new Error("unhandled output format: " + args["format"]));
}
else {
const output_content : string = await output.render(input_data);
process.stdout.write(output_content);
}
}
}
}
}
}
_sindri.main(process.argv.slice(2));

View file

@ -0,0 +1,339 @@
namespace _sindri.outputs.backend.typescript
{
/**
*/
async function get_template(
name : string
) : Promise<string>
{
return _sindri.get_template(_sindri.enum_realm.backend, "typescript", name + ".ts.tpl");
}
/**
*/
async function coin(
template_name : string,
values : Record<string, string>
) : Promise<string>
{
return lib_plankton.string.coin(
await get_template(template_name),
values,
{
"open": "<<",
"close": ">>",
}
);
}
/**
*/
export async function render(
input_data
) : Promise<string>
{
// TODO as command line argument?
const conf_internal : {
namespace_base : string;
} = {
"namespace_base": "_sindri.",
};
const map_primitive_type = function (typename : string) : lib_plankton.prog.struct_type {
const mymap : Record<string, lib_plankton.prog.struct_type> = {
"boolean": new lib_plankton.prog.struct_type_boolean(),
"integer": new lib_plankton.prog.struct_type_integer(),
// "float": new lib_plankton.prog.struct_type_integer(),
"string_short": new lib_plankton.prog.struct_type_string(),
"string_medium": new lib_plankton.prog.struct_type_string(),
"string_long": new lib_plankton.prog.struct_type_string(),
};
return mymap[typename];
};
const name_table = function (domain) : string {return (domain.name);};
const database_path = lib_plankton.string.coin(
"/tmp/{{name}}.sqlite",
{
"name": "sindri",
}
);
const namespace_entity = function (domain) {
return lib_plankton.string.coin(
"{{base}}entities.{{domain_name}}",
{
"base": conf_internal.namespace_base,
"domain_name": domain.name,
}
);
};
const name_entity_type = function (fully_qualified, domain) : string {
return lib_plankton.string.coin(
"{{prefix}}type_value",
{
"prefix": (
fully_qualified
? (namespace_entity(domain) + ".")
: ""
),
}
);
};
const namespace_repository = function (domain) {
return lib_plankton.string.coin(
"{{base}}repositories",
{
"base": conf_internal.namespace_base,
}
);
};
const name_repository_function = function (fully_qualified, domain, action) : string {
return lib_plankton.string.coin(
"{{prefix}}{{action}}",
{
"prefix": (
fully_qualified
? namespace_repository(domain)
: ""
),
"action": action,
}
);
};
return coin(
"master",
{
"namespace_base": conf_internal.namespace_base,
"entities": (
(await Promise.all(
input_data["domains"]
.map(
(domain) => coin(
"entity",
{
"domain_name": domain.name,
"defs": lib_plankton.prog.typescript.render_statement(
new lib_plankton.prog.struct_statement_type_definition(
name_entity_type(false, domain),
new lib_plankton.prog.struct_type_record(
domain.data_fields
.map(
(data_field) => ({
"name": data_field.name,
"type": (
data_field.nullable
? new lib_plankton.prog.struct_type_union(
new lib_plankton.prog.struct_type_literal(
new lib_plankton.prog.struct_expression_literal(
null
)
),
map_primitive_type(data_field["type"])
)
: map_primitive_type(data_field["type"])
),
"mandatory": true,
})
)
),
{
"export": true,
}
),
{
"level": 2,
}
)
}
)
)
))
.join("")
),
"repositories": (
(await Promise.all(
input_data["domains"]
.map(
(domain) => coin(
"repository",
{
"domain_name": domain.name,
"type_name": name_entity_type(true, domain),
"table_name": name_table(domain),
"list_function_name": name_repository_function(false, domain, "list"),
"list_query_fields": (
[domain.key_field.name]
.concat(domain.data_fields.map(field => field.name))
.join(",")
),
"list_result": lib_plankton.prog.typescript.render_expression(
new lib_plankton.prog.struct_expression_dict(
[
{
"key": "key",
"value": new lib_plankton.prog.struct_expression_projection(
new lib_plankton.prog.struct_expression_variable("row"),
new lib_plankton.prog.struct_expression_literal(domain.key_field.name)
),
},
{
"key": "value",
"value": new lib_plankton.prog.struct_expression_dict(
domain.data_fields
.map(
(field, index) => ({
"key": field.name,
"value": new lib_plankton.prog.struct_expression_projection(
new lib_plankton.prog.struct_expression_variable("row"),
new lib_plankton.prog.struct_expression_literal(field.name)
)
})
)
),
},
]
),
{
"indent": false,
"level": 8,
}
),
"read_function_name": name_repository_function(false, domain, "read"),
"read_query_fields": (
[]
.concat(domain.data_fields.map(field => field.name))
.join(",")
),
"read_result_fields": lib_plankton.prog.typescript.render_expression(
new lib_plankton.prog.struct_expression_dict(
domain.data_fields
.map(
(field, index) => ({
"key": field.name,
"value": new lib_plankton.prog.struct_expression_projection(
new lib_plankton.prog.struct_expression_variable("row"),
new lib_plankton.prog.struct_expression_literal(field.name)
)
})
)
),
{
"indent": false,
"level": 6,
}
),
"create_function_name": name_repository_function(false, domain, "create"),
"create_query_field_names": (
domain.data_fields
.map(field => field.name)
.join(",")
),
"create_query_field_placeholders": (
domain.data_fields
.map(field => (":" + field.name))
.join(",")
),
"create_query_field_values": lib_plankton.prog.typescript.render_expression(
new lib_plankton.prog.struct_expression_dict(
domain.data_fields
.map(
field => ({
"key": field.name,
"value": new lib_plankton.prog.struct_expression_fieldaccess(
new lib_plankton.prog.struct_expression_variable("value"),
field.name,
),
})
)
),
{
"indent": false,
"level": 6,
}
),
"update_function_name": name_repository_function(false, domain, "update"),
"update_query_assignments": (
domain.data_fields
.map(
field => lib_plankton.string.coin(
"{{key}} = {{value}}",
{
"key": field.name,
"value": (":" + ("value_" + field.name)),
}
)
)
.join(", ")
),
"update_query_values": lib_plankton.prog.typescript.render_expression(
new lib_plankton.prog.struct_expression_dict(
[]
.concat(
[
{
"key": "key",
"value": new lib_plankton.prog.struct_expression_variable("key"),
},
]
)
.concat(
domain.data_fields
.map(
field => ({
"key": ("value_" + field.name),
"value": new lib_plankton.prog.struct_expression_fieldaccess(
new lib_plankton.prog.struct_expression_variable("value"),
field.name,
),
})
)
)
),
{
"indent": false,
"level": 7,
}
),
"delete_function_name": name_repository_function(false, domain, "delete"),
}
)
)
))
.join("")
),
"api": (
(await Promise.all(
input_data["domains"]
.map(
(domain) => coin(
"api",
{
"domain_name": domain.name,
"type_name": name_entity_type(true, domain),
"repository_function_list": name_repository_function(true, domain, "list"),
"repository_function_read": name_repository_function(true, domain, "read"),
"repository_function_create": name_repository_function(true, domain, "create"),
"repository_function_update": name_repository_function(true, domain, "update"),
"repository_function_delete": name_repository_function(true, domain, "delete"),
}
)
)
))
.join("")
),
}
);
}
}
_sindri.add_output(
_sindri.enum_realm.backend,
"typescript",
{
"render": _sindri.outputs.backend.typescript.render,
}
);

View file

@ -0,0 +1,118 @@
// <<domain_name>>
{
lib_plankton.rest.register(
rest,
lib_plankton.http.enum_method.get,
lib_plankton.string.coin(
"/{{base_path}}{{domain_name}}",
{
"base_path": _brock.conf.api_base_path,
"domain_name": "<<domain_name>>",
}
),
{
"execution": async function (stuff) {
return {
"status_code": 200,
"data": await <<repository_function_list>>(
)
};
}
}
);
lib_plankton.rest.register(
rest,
lib_plankton.http.enum_method.get,
lib_plankton.string.coin(
"/{{base_path}}{{domain_name}}/:id",
{
"base_path": _brock.conf.api_base_path,
"domain_name": "<<domain_name>>",
}
),
{
"execution": async function (stuff) {
return {
"status_code": 200,
"data": await <<repository_function_read>>(
parseInt(
stuff.path_parameters["id"]
)
)
};
}
}
);
lib_plankton.rest.register(
rest,
lib_plankton.http.enum_method.post,
lib_plankton.string.coin(
"/{{base_path}}{{domain_name}}",
{
"base_path": _brock.conf.api_base_path,
"domain_name": "<<domain_name>>",
}
),
{
"execution": async function (stuff) {
const id = await <<repository_function_create>>(
(stuff.input as <<type_name>>)
);
return {
"status_code": 201,
"data": id
};
}
}
);
lib_plankton.rest.register(
rest,
lib_plankton.http.enum_method.patch,
lib_plankton.string.coin(
"/{{base_path}}{{domain_name}}/:id",
{
"base_path": _brock.conf.api_base_path,
"domain_name": "<<domain_name>>",
}
),
{
"execution": async function (stuff) {
const dummy = await <<repository_function_create>>(
parseInt(
stuff.path_parameters["id"]
),
(stuff.input as <<type_name>>)
);
return {
"status_code": 200,
"data": null
};
}
}
);
lib_plankton.rest.register(
rest,
lib_plankton.http.enum_method.delete,
lib_plankton.string.coin(
"/{{base_path}}{{domain_name}}/:id",
{
"base_path": _brock.conf.api_base_path,
"domain_name": "<<domain_name>>",
}
),
{
"execution": async function (stuff) {
const dummy = await <<repository_function_delete>>(
parseInt(
stuff.path_parameters["id"]
)
);
return {
"status_code": 200,
"data": null
};
}
}
);
}

View file

@ -0,0 +1,4 @@
export namespace <<domain_name>>
{
<<defs>>
}

View file

@ -0,0 +1,74 @@
// declare var require;
namespace <<namespace_base>>entities
{
<<entities>>
}
namespace <<namespace_base>>repositories
{
<<repositories>>
}
namespace <<namespace_base>>main
{
// run
export function run(
) : void
{
lib_plankton.log.conf_push(
[
new lib_plankton.log.class_channel_stdout(
)
]
);
// define api
{
// meta
{
lib_plankton.rest.register(
rest,
lib_plankton.http.enum_method.get,
"/_spec",
{
"execution": async function (stuff) {
return {
"status_code": 200,
"data": lib_plankton.rest.to_oas(
rest
)
};
}
}
);
}
<<api>>
}
// setup server
const server = lib_plankton.server.make(
_brock.conf.server_port,
async function (input) {
const http_request : lib_plankton.http.type_request = lib_plankton.http.decode_request(
input
);
const http_response : lib_plankton.http.type_response = await lib_plankton.rest.call(
rest,
http_request
);
return lib_plankton.http.encode_response(
http_response
);
}
);
// start
lib_plankton.server.start(
server
);
}
}

View file

@ -0,0 +1,113 @@
export namespace <<domain_name>>
{
// list
export function <<list_function_name>>(
) : Promise<Array<{key : number; value : <<type_name>>;}>>
{
return (
lib_plankton.sqlite.query_get(
_brock.conf.database_path,
{
"template": "SELECT <<list_query_fields>> FROM <<table_name>>;",
"arguments": {
}
}
)
.then(
(rows) => rows.map(
(row) => <<list_result>>
)
)
);
}
// read
export function <<read_function_name>>(
key : number
) : Promise<<<type_name>>>
{
return (
lib_plankton.sqlite.query_get(
_brock.conf.database_path,
{
"template": "SELECT <<read_query_fields>> FROM <<table_name>> WHERE (id = :key);",
"arguments": {
"key": key
}
}
)
.then(
(rows) {
const row = rows[0];
return <<read_result_fields>>;
}
)
);
}
// create
export function <<create_function_name>>(
value : <<type_name>>
) : Promise<number>
{
return (
lib_plankton.sqlite.query_put(
_brock.conf.database_path,
{
"template": "INSERT INTO <<table_name>>(<<create_query_field_names>>) VALUES (<<create_query_field_placeholders>>);",
"arguments": <<create_query_field_values>>
}
)
.then(
(result) => result.id
)
);
}
// update
export function <<update_function_name>>(
key : number,
value : <<type_name>>
) : Promise<void>
{
return (
lib_plankton.sqlite.query_put(
_brock.conf.database_path,
{
"template": "UPDATE <<table_name>> SET <<update_query_assignments>> WHERE (id = :key);",
"arguments": {
"key": key,
<<update_query_values>>
}
}
)
.then(
(result) => {}
)
);
}
// delete
export function <<delete_function_name>>(
key : number
) : Promise<void>
{
return (
lib_plankton.sqlite.query_put(
_brock.conf.database_path,
{
"template": "DELETE FROM <<table_name>> WHERE (id = :key);",
"arguments": {
"key": key
}
}
)
.then(
(result) => {}
)
);
}
}

View file

@ -0,0 +1,221 @@
namespace _sindri.outputs.database.mysql
{
/**
*/
function value_encode(
value : any
) : string
{
if (value === null) {
return "NULL";
}
else {
switch (typeof(value)) {
case "boolean": {
return (value ? "TRUE" : "FALSE");
break;
}
case "number": {
return value.toString();
break;
}
case "string": {
return ("'" + value + "'");
break;
}
default: {
throw (new Error("unhandled"));
break;
}
}
}
}
/**
*/
export function render(
input_data
) : string
{
return (
input_data.domains
.map(
(domain) => lib_plankton.string.coin(
"CREATE TABLE\n\t`{{name}}`(\n{{entries}}\n\t){{comment}}\n;",
{
"name": domain.name,
"comment": (
(domain.description === null)
? ""
: lib_plankton.string.coin(
" COMMENT '{{comment}}'",
{
"comment": domain.description,
}
)
),
"entries": (
(
[]
// key field
.concat(
(domain.key_field === null)
? []
: lib_plankton.string.coin(
"`{{name}}` {{parameters}}",
{
"name": domain.key_field.name,
"parameters": (
[
"INTEGER",
"PRIMARY KEY",
"AUTO INCREMENT",
]
.concat(
(domain.key_field.description === null)
? []
: [
lib_plankton.string.coin(
"COMMENT '{{comment}}'",
{
"comment": domain.key_field.description,
}
),
]
)
.join(" ")
),
}
)
)
// data fields
.concat(
domain.data_fields
.map(
(data_field) => lib_plankton.string.coin(
"`{{name}}` {{parameters}}",
{
"name": data_field.name,
"parameters": (
(
// type
[
{
"boolean": "BOOLEAN",
"integer": "INTEGER",
"string_short": "VARCHAR(63)",
"string_medium": "VARCHAR(255)",
"string_long": "TEXT",
}[data_field.type],
]
// nullability
.concat(
data_field.nullable
? ["NULL"]
: []
)
// default
.concat(
(data_field.default === undefined)
? []
: [
lib_plankton.string.coin(
"DEFAULT {{value}}",
{
"value": value_encode(data_field.default),
}
),
]
)
// comment
.concat(
(data_field.description === null)
? []
: [
lib_plankton.string.coin(
"COMMENT '{{comment}}'",
{
"comment": data_field.description,
}
),
]
)
)
.join(" ")
)
}
)
)
)
// constraints
.concat(
domain["constraints"]
.map(
(constraint) => {
switch (constraint.kind) {
default: {
throw (new Error("unhandled constraint kind: " + constraint.kind));
break;
}
case "foreign_key": {
return lib_plankton.string.coin(
"FOREIGN KEY ({{fields}}) REFERENCES `{{reference_name}}`({{reference_fields}})",
{
"fields": (
constraint.parameters["fields"]
.map(x => ('`' + x + '`'))
.join(",")
),
"reference_name": constraint.parameters["reference"]["name"],
"reference_fields": (
constraint.parameters["reference"]["fields"]
.map(x => ('`' + x + '`'))
.join(",")
),
}
);
break;
}
case "unique": {
return lib_plankton.string.coin(
"UNIQUE ({{fields}})",
{
"fields": (
constraint.parameters["fields"]
.map(x => ('`' + x + '`'))
.join(",")
),
}
);
break;
}
}
}
)
)
)
.map(x => ("\t\t" + x))
.join(",\n")
),
}
)
)
.map(x => (x + "\n"))
.join("\n")
);
}
}
/**
*/
_sindri.add_output(
_sindri.enum_realm.database,
"mysql",
{
"render": (x) => Promise.resolve<string>(_sindri.outputs.database.mysql.render(x)),
}
);

View file

@ -0,0 +1,255 @@
namespace _sindri.outputs.database.postgresql
{
/**
*/
function value_encode(
value : any
) : string
{
if (value === null) {
return "NULL";
}
else {
switch (typeof(value)) {
case "boolean": {
return (value ? "TRUE" : "FALSE");
break;
}
case "number": {
return value.toString();
break;
}
case "string": {
return ("'" + value + "'");
break;
}
default: {
throw (new Error("unhandled"));
break;
}
}
}
}
/**
*/
export function render(
input_data
) : string
{
return (
input_data.domains
.map(
(domain) => lib_plankton.string.coin(
"CREATE TABLE\n\t{{name}}(\n{{entries}}\n\t)\n;\n{{comments}}",
{
"name": domain.name,
"entries": (
(
[]
// key field
.concat(
(domain.key_field === null)
? []
: lib_plankton.string.coin(
"{{name}} {{parameters}}",
{
"name": ('"' + domain.key_field.name + '"'),
"parameters": (
[
"SERIAL",
]
.join(" ")
),
}
)
)
// data fields
.concat(
domain.data_fields
.map(
(data_field) => lib_plankton.string.coin(
"{{name}} {{parameters}}",
{
"name": ('"' + data_field.name + '"'),
"parameters": (
(
// type
[
{
"boolean": "BOOLEAN",
"integer": "INTEGER",
"string_short": "VARCHAR(63)",
"string_medium": "VARCHAR(255)",
"string_long": "TEXT",
}[data_field.type],
]
// nullability
.concat(
data_field.nullable
? []
: ["NOT NULL"]
)
// default
.concat(
(data_field.default === undefined)
? []
: [
lib_plankton.string.coin(
"DEFAULT {{value}}",
{
"value": value_encode(data_field.default),
}
),
]
)
)
.join(" ")
)
}
)
)
)
// constraints
.concat(
domain["constraints"]
.concat(
(domain.key_field === null)
?
[]
:
[
{
"kind": "unique",
"parameters": {
"fields": [
domain.key_field.name,
]
}
},
]
)
.map(
(constraint) => {
switch (constraint.kind) {
default: {
throw (new Error("unhandled constraint kind: " + constraint.kind));
break;
}
case "foreign_key": {
return lib_plankton.string.coin(
"FOREIGN KEY ({{fields}}) REFERENCES {{reference_name}}({{reference_fields}})",
{
"fields": (
constraint.parameters["fields"]
.map(x => ('"' + x + '"'))
.join(",")
),
"reference_name": ('"' + constraint.parameters["reference"]["name"] + '"'),
"reference_fields": (
constraint.parameters["reference"]["fields"]
.map(x => ('"' + x + '"'))
.join(",")
),
}
);
break;
}
case "unique": {
return lib_plankton.string.coin(
"UNIQUE ({{fields}})",
{
"fields": (
constraint.parameters["fields"]
.map(x => ('"' + x + '"'))
.join(",")
),
}
);
break;
}
}
}
)
)
)
.map(x => ("\t\t" + x))
.join(",\n")
),
"comments": (
(
[]
.concat(
(! (domain.description === null))
? [
{
"kind": "TABLE",
"subject": domain.name,
"value": domain.description,
}
]
: []
)
.concat(
(
(! (domain.key_field === null))
&&
(! (domain.key_field.description === null))
)
? [
{
"kind": "COLUMN",
"subject": (domain.name + "." + domain.key_field.name),
"value": domain.key_field.description,
}
]
: []
)
.concat(
domain.data_fields
.filter(
data_field => (! (data_field.description === null))
)
.map(
data_field => ({
"kind": "COLUMN",
"subject": (domain.name + "." + data_field.name),
"value": data_field.description,
})
)
)
)
.map(
entry => lib_plankton.string.coin(
"COMMENT ON {{kind}} {{subject}} IS '{{value}}';",
{
"kind": entry.kind,
"subject": entry.subject,
"value": entry.value,
}
)
)
.join("\n")
),
}
)
)
.map(x => (x + "\n"))
.join("\n")
);
}
}
/**
*/
_sindri.add_output(
_sindri.enum_realm.database,
"postgresql",
{
"render": (x) => Promise.resolve<string>(_sindri.outputs.database.postgresql.render(x)),
}
);

View file

@ -0,0 +1,183 @@
namespace _sindri.outputs.database.sqlite
{
/**
*/
function value_encode(
value : any
) : string
{
if (value === null) {
return "NULL";
}
else {
switch (typeof(value)) {
case "boolean": {
return (value ? "TRUE" : "FALSE");
break;
}
case "number": {
return value.toString();
break;
}
case "string": {
return ("'" + value + "'");
break;
}
default: {
throw (new Error("unhandled"));
break;
}
}
}
}
/**
*/
export function render(
input_data
) : string
{
return (
input_data["domains"]
.map(
(domain) => lib_plankton.string.coin(
"CREATE TABLE\n\t`{{name}}`(\n{{entries}}\n\t)\n;",
{
"name": domain.name,
"entries": (
(
[]
// key field
.concat(
(domain.key_field === null)
? []
: lib_plankton.string.coin(
"`{{name}}` {{parameters}}",
{
"name": domain.key_field.name,
"parameters": (
[
"INTEGER",
"PRIMARY KEY",
"AUTOINCREMENT",
]
.join(" ")
),
}
)
)
// data fields
.concat(
domain.data_fields
.map(
(data_field) => lib_plankton.string.coin(
"`{{name}}` {{parameters}}",
{
"name": data_field.name,
"parameters": (
(
// type
[
{
"boolean": "BOOLEAN",
"integer": "INTEGER",
"string_short": "VARCHAR(63)",
"string_medium": "VARCHAR(255)",
"string_long": "TEXT",
}[data_field.type],
]
// nullability
.concat(
data_field.nullable
? []
: ["NOT NULL"]
)
// default
.concat(
(data_field.default === undefined)
? []
: [
lib_plankton.string.coin(
"DEFAULT {{value}}",
{
"value": value_encode(data_field.default),
}
),
]
)
)
.join(" ")
)
}
)
)
)
// constraints
.concat(
domain.constraints
.map(
(constraint) => {
switch (constraint.kind) {
default: {
throw (new Error("unhandled constraint kind: " + constraint.kind));
break;
}
case "foreign_key": {
return lib_plankton.string.coin(
"FOREIGN KEY ({{fields}}) REFERENCES `{{reference_name}}`({{reference_fields}})",
{
"fields": (
constraint.parameters["fields"]
.map(x => ('`' + x + '`'))
.join(",")
),
"reference_name": constraint.parameters["reference"]["name"],
"reference_fields": (
constraint.parameters["reference"]["fields"]
.map(x => ('`' + x + '`'))
.join(",")
),
}
);
break;
}
case "unique": {
return lib_plankton.string.coin(
"UNIQUE ({{fields}})",
{
"fields": (
constraint.parameters["fields"]
.map(x => ('`' + x + '`'))
.join(",")
),
}
);
break;
}
}
}
)
)
)
.map(x => ("\t\t" + x))
.join(",\n")
),
}
)
)
.map(x => (x + "\n"))
.join("\n")
);
}
}
_sindri.add_output(
_sindri.enum_realm.database,
"sqlite",
{
"render": (x) => Promise.resolve<string>(_sindri.outputs.database.sqlite.render(x)),
}
);

View file

@ -0,0 +1,37 @@
namespace _sindri.outputs.frontend.typescript
{
/**
*/
async function get_template(
name : string
) : Promise<string>
{
return _sindri.get_template(_sindri.enum_realm.frontend, "typescript", name);
}
/**
*/
export async function render(
input_data
) : Promise<string>
{
return lib_plankton.string.coin(
await get_template("core.ts.tpl"),
{
"value": JSON.stringify(input_data, undefined, "\t"),
}
);
}
}
_sindri.add_output(
_sindri.enum_realm.frontend,
"typescript",
{
"render": _sindri.outputs.frontend.typescript.render,
}
);

View file

@ -0,0 +1,251 @@
namespace _sindri
{
/**
*/
const abstract = {{value}};
/**
* @todo headers
* @todo query
*/
export function api_call(
method : string,
path : string,
input : any = null
) : Promise<any>
{
return (
fetch(
(conf.backend.scheme + "://" + conf.backend.host + ":" + conf.backend.port.toFixed() + path),
{
"method": method,
"headers": {
"Content-Type": "application/json",
},
"body": (
(input === null)
? undefined
: /*Buffer.from*/(JSON.stringify(input))
),
}
)
.then(
x => x.json()
)
);
}
/**
*/
export function editor(
domain_description : any,
hook_switch : (null | ((state : lib_plankton.zoo_editor.type_state<int, any>) => void))
) : lib_plankton.zoo_editor.type_editor<int, any>
{
return lib_plankton.zoo_editor.make<int, any>(
// store
{
"setup": () => Promise.resolve<void>(undefined),
"search": (term) => (
api_call(
"GET",
lib_plankton.string.coin(
"/{{name}}",
{
"name": domain_description.name,
}
),
null
)
.then(
(entries) => Promise.resolve<Array<{key : int; preview : any}>>(
entries
.filter(
entry => (
entry.key.toFixed().includes(term.toLowerCase())
||
(
/*
(term.length >= 3)
&&
*/
domain_description.data_fields
.some(
data_field => JSON.stringify(entry.value[data_field.name]).toLowerCase().includes(term.toLowerCase())
)
)
)
)
.map(
entry => ({
"key": entry.key,
"preview": entry.value,
})
)
)
)
),
"read": (id) => api_call(
"GET",
lib_plankton.string.coin(
"/{{name}}/{{id}}",
{
"name": domain_description.name,
"id": id.toFixed(0),
}
),
null
),
"create": (object) => api_call(
"POST",
lib_plankton.string.coin(
"/{{name}}",
{
"name": domain_description.name,
}
),
object
),
"update": (id, object) => api_call(
"PATCH",
lib_plankton.string.coin(
"/{{name}}/{{id}}",
{
"name": domain_description.name,
"id": id.toFixed(0),
}
),
object
),
"delete": (id) => api_call(
"DELETE",
lib_plankton.string.coin(
"/{{name}}/{{id}}",
{
"name": domain_description.name,
"id": id.toFixed(0),
}
),
null
),
},
// form
lib_plankton.zoo_form.make<any>(
// method
"GET",
// fields
(
domain_description.data_fields.map(
data_field => ({
"name": data_field.name,
"type": {
"string_short": "text",
"string_medium": "text",
"string_long": "text",
"integer": "number",
"float": "number",
}[data_field.type],
})
)
),
// encode
(object) => object,
// decode
(object_encoded) => object_encoded,
),
// options
{
"hook_switch": hook_switch,
}
);
}
/**
*/
export function edit_location_name(
domain_description
) : string
{
return lib_plankton.string.coin(
"edit_{{name}}",
{
"name": domain_description.name,
}
);
}
/**
*/
export function register_editor_page_for_domain(
domain_description
) : void
{
const location_name : string = edit_location_name(domain_description);
lib_plankton.zoo_page.register(
location_name,
async (parameters, element_main) => {
const id : (null | int) = ((! ("id" in parameters)) ? null : parseInt(parameters["id"]));
const mode : lib_plankton.zoo_editor.enum_mode = editor_decode_mode(parameters["mode"], id);
const search_term : (null | string) = (parameters["search"] ?? null);
lib_plankton.zoo_editor.render<int, any>(
editor(
domain_description,
(state) => {
lib_plankton.zoo_page.set(
{
"name": location_name,
"parameters": {
"mode": state.mode,
"id": ((state.key === null) ? null : state.key.toFixed(0)),
"search": state.search_state.term,
},
}
);
}
),
element_main,
{
"state": {
"mode": mode,
"key": id,
"search_state": {"term": search_term},
}
}
);
}
);
lib_plankton.zoo_page.add_nav_entry(
{
"name": location_name,
"parameters": {
"mode": "find",
"key": null,
"search": ""
}
},
{
"label": (domain_description.name + "s"),
}
);
}
/**
*/
export function init(
) : Promise<void>
{
abstract.domains.forEach(
domain_description => {
register_editor_page_for_domain(domain_description);
}
);
return Promise.resolve<void>(undefined);
}
}

View file

@ -0,0 +1,103 @@
namespace _sindri.outputs.other.jsonschema
{
/**
*/
export function render(
input_data
) : string
{
return lib_plankton.json.encode(
Object.fromEntries(
input_data.domains.map(
domain => ([
domain.name,
{
"type": ["array"],
"items": {
"type": ["object"],
"additionalProperties": false,
"properties": Object.fromEntries(
[]
.concat(
(domain.key_field === null)
? []
: [
[
domain.key_field.name,
{
"type": ["integer"],
"description": (domain.key_field.description ?? undefined),
}
]
]
)
.concat(
domain.data_fields
.map(
data_field => ([
data_field.name,
{
"type": (
[]
.concat(
data_field.nullable
? ["null"]
: []
)
.concat(
[
{
"boolean": "boolean",
"integer": "integer",
"float": "number",
"string_short": "string",
"string_medium": "string",
"string_long": "string",
}[data_field.type]
]
)
),
"description": (data_field.description ?? undefined),
}
])
)
)
),
"required": (
[]
.concat(
(domain.key_field === null)
? []
: [domain.key_field.name]
)
.concat(
domain.data_fields
.map(
data_field => data_field.name
)
)
)
},
}
])
)
),
{
"formatted": true
}
);
}
}
/**
*/
_sindri.add_output(
_sindri.enum_realm.other,
"jsonschema",
{
"render": (x) => Promise.resolve<string>(_sindri.outputs.other.jsonschema.render(x)),
}
);

3
todo.md Normal file
View file

@ -0,0 +1,3 @@
- Nutzer-Verwaltung
- auf `plankton:database` setzen

4
tools/build Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env sh
make --file=tools/makefile

3
tools/clear Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env sh
rm --recursive --force temp build

15
tools/get-plankton Executable file
View file

@ -0,0 +1,15 @@
#!/usr/bin/env sh
modules=""
modules="${modules} base"
# modules="${modules} call"
modules="${modules} string"
modules="${modules} json"
modules="${modules} file"
modules="${modules} prog"
modules="${modules} args"
mkdir -p lib/plankton
cd lib/plankton
ptk bundle node ${modules}
cd -

3
tools/install Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env sh
cp build/sindri /usr/local/bin/sindri

44
tools/makefile Normal file
View file

@ -0,0 +1,44 @@
## commands
cmd_create_directory := mkdir --parents
cmd_typescript_compile := tsc
cmd_concatenate := cat
cmd_chmod := chmod
cmd_echo := echo -e
cmd_echox := echo
cmd_log := echo -e "--"
cmd_copy := cp -r -u -v
## rules
.PHONY: all
all: build/sindri templates
temp/sindri-unlinked.js: \
lib/plankton/plankton.d.ts \
source/base.ts \
source/outputs/other/jsonschema/logic.ts \
source/outputs/database/sqlite/logic.ts \
source/outputs/database/mysql/logic.ts \
source/outputs/database/postgresql/logic.ts \
source/outputs/backend/typescript/logic.ts \
source/outputs/frontend/typescript/logic.ts \
source/conf.ts \
source/main.ts
@ ${cmd_log} "compiling …"
@ ${cmd_create_directory} temp
@ ${cmd_typescript_compile} $^ --lib es2020 --target es6 --outFile $@
build/sindri: lib/plankton/plankton.js temp/sindri-unlinked.js
@ ${cmd_log} "linking …"
@ ${cmd_create_directory} build
@ ${cmd_echox} "#!/usr/bin/env node" > temp/head.js
@ ${cmd_concatenate} temp/head.js $^ > $@
@ ${cmd_chmod} +x $@
.PHONY: templates
templates: \
source/outputs/frontend/typescript/templates/core.ts.tpl
@ ${cmd_log} "placing templates …"
@ tools/place-templates

17
tools/place-templates Executable file
View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
## consts
dir_from=source/outputs
dir_to=build/templates
## exec
paths=$(find ${dir_from} -type d -name templates)
for path in ${paths}
do
type=$(echo ${path} | cut --delimiter='/' --fields='3-' | cut --delimiter='/' --fields='-2')
mkdir --parents ${dir_to}/${type}
cp --update --verbose ${path}/* ${dir_to}/${type}/
done