@appweaver/create-weaver-app 1.0.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/LICENSE +1 -0
- package/README.md +7 -0
- package/create-weaver-app.d.ts +2 -0
- package/create-weaver-app.js +266 -0
- package/package.json +37 -0
- package/skill/GUIDELINES.md +298 -0
- package/skill/SKILL.md +593 -0
- package/skill/references/cache.md +207 -0
- package/skill/references/cli.md +213 -0
- package/skill/references/client.md +507 -0
- package/skill/references/configuration.md +402 -0
- package/skill/references/database.md +134 -0
- package/skill/references/dependency-injection.md +214 -0
- package/skill/references/events.md +152 -0
- package/skill/references/mailer.md +235 -0
- package/skill/references/queue.md +196 -0
- package/skill/references/resources.md +961 -0
- package/skill/references/scheduler.md +184 -0
- package/skill/references/security.md +694 -0
- package/skill/references/storage.md +251 -0
- package/templates/default/.dockerignore +5 -0
- package/templates/default/.env.tpl +1 -0
- package/templates/default/.prettierignore +3 -0
- package/templates/default/.prettierrc +7 -0
- package/templates/default/Dockerfile +56 -0
- package/templates/default/Dockerfile.bun +56 -0
- package/templates/default/README.md.tpl +7 -0
- package/templates/default/appweaver.dev.json.tpl +9 -0
- package/templates/default/appweaver.json.bun.tpl +17 -0
- package/templates/default/appweaver.json.tpl +16 -0
- package/templates/default/appweaver.test.json.tpl +30 -0
- package/templates/default/bunfig.toml.bun +8 -0
- package/templates/default/database/client.ts.tpl +7 -0
- package/templates/default/database/schema.prisma +13 -0
- package/templates/default/database/seeders/001-create-admin-user.ts.tpl +39 -0
- package/templates/default/eslint.config.mjs +55 -0
- package/templates/default/eslint.config.mjs.bun +53 -0
- package/templates/default/jest.config.json.node +23 -0
- package/templates/default/package.json.bun.tpl +39 -0
- package/templates/default/package.json.tpl +44 -0
- package/templates/default/prisma.config.ts.tpl +14 -0
- package/templates/default/public/favicon.ico +0 -0
- package/templates/default/public/robots.txt +2 -0
- package/templates/default/src/features/index.ts.tpl +0 -0
- package/templates/default/src/main.ts.tpl +7 -0
- package/templates/default/src/resources/user/model.ts.tpl +28 -0
- package/templates/default/src/resources/user/policy.ts.tpl +3 -0
- package/templates/default/src/resources/user/routes.ts.tpl +3 -0
- package/templates/default/src/resources/user/service.ts.tpl +16 -0
- package/templates/default/src/types/generated.ts.tpl +1 -0
- package/templates/default/src/types/index.ts.tpl +1 -0
- package/templates/default/start.sh +26 -0
- package/templates/default/start.sh.bun +26 -0
- package/templates/default/swc.config.json.node +13 -0
- package/templates/default/test/e2e/jest.e2e-config.json.node +22 -0
- package/templates/default/test/e2e/main.test.ts.tpl +24 -0
- package/templates/default/test/e2e/support/each.ts.tpl +13 -0
- package/templates/default/test/e2e/support/preload.ts.bun +13 -0
- package/templates/default/test/e2e/support/setup.ts.tpl +13 -0
- package/templates/default/test/e2e/support/teardown.ts.tpl +13 -0
- package/templates/default/test/unit/sample.test.ts.tpl +5 -0
- package/templates/default/tsconfig.build.json +10 -0
- package/templates/default/tsconfig.json +27 -0
- package/templates/default/tsconfig.json.bun +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
UNLICENSED
|
package/README.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
const commander_1 = require("commander");
|
|
12
|
+
const glob_1 = require("glob");
|
|
13
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(__dirname, './package.json'), 'utf8'));
|
|
14
|
+
const prismaVersion = '7.8.0';
|
|
15
|
+
const dbTypes = ['sqlite', 'postgresql', 'mysql', 'sqlserver'];
|
|
16
|
+
const agentTypes = [
|
|
17
|
+
'claude',
|
|
18
|
+
'codex',
|
|
19
|
+
'junie',
|
|
20
|
+
'cursor',
|
|
21
|
+
'copilot',
|
|
22
|
+
'opencode',
|
|
23
|
+
'kiro',
|
|
24
|
+
'pi',
|
|
25
|
+
'none'
|
|
26
|
+
];
|
|
27
|
+
const program = new commander_1.Command();
|
|
28
|
+
program
|
|
29
|
+
.name('create-weaver-app')
|
|
30
|
+
.description('Create Weaver App - Bootstrap new Appweaver project')
|
|
31
|
+
.version(pkg.version, '-v, --version', 'Output the current version.')
|
|
32
|
+
.helpOption('-h, --help', 'Output usage information.')
|
|
33
|
+
.usage('<name> [description] [options]')
|
|
34
|
+
.argument('<name>', 'Name of the new project')
|
|
35
|
+
.argument('[description]', 'Description of the new project', 'Appweaver project')
|
|
36
|
+
.option('-o, --outputDir [outputDir]', 'Directory where to generate new project. (default: name of project)')
|
|
37
|
+
.option('--database [database]', `Type of SQL database (${dbTypes.join(', ')}).`, parseDatabaseType, 'sqlite')
|
|
38
|
+
.option('--port [port]', 'Port number where the application server will listen.', parsePortNumber, 5000)
|
|
39
|
+
.option('--host [host]', 'Hostname or IP address where the application server will bind.', parseHostname, '0.0.0.0')
|
|
40
|
+
.option('--agent [agent]', `The AI agent for which to configure guidelines and skill files (${agentTypes.join(', ')}).`, parseAgentType, 'claude')
|
|
41
|
+
.option('--bun', 'Use Bun as application runtime.')
|
|
42
|
+
.option('--skipInstall', 'Skip all dependencies installation.')
|
|
43
|
+
.option('--noRedis', 'Skip IoRedis package installation.')
|
|
44
|
+
.option('--noQueue', 'Skip BullQueue package installation.')
|
|
45
|
+
.option('--noMailer', 'Skip Nodemailer package installation.')
|
|
46
|
+
.option('--noCron', 'Skip Cron package installation.')
|
|
47
|
+
.action(async (name, description, _, command) => {
|
|
48
|
+
const directory = command.getOptionValue('outputDir');
|
|
49
|
+
const runtime = command.getOptionValue('bun') ? 'bun' : 'node';
|
|
50
|
+
const packageManager = runtime === 'bun' ? 'bun' : 'npm';
|
|
51
|
+
// Check if bun runtime is installed on this machine
|
|
52
|
+
if (runtime === 'bun') {
|
|
53
|
+
const status = await runProcess('bun', ['--version'], { quiet: true });
|
|
54
|
+
if (status !== 0) {
|
|
55
|
+
console.error('Bun runtime is not installed on this machine.');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Sanitize and create a new directory
|
|
60
|
+
const sanitizedName = name
|
|
61
|
+
.replace(/\s+/g, '-')
|
|
62
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
63
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
64
|
+
.toLowerCase();
|
|
65
|
+
const projectDir = directory ?? sanitizedName;
|
|
66
|
+
const destDir = node_path_1.default.join(process.cwd(), projectDir);
|
|
67
|
+
// Initialize new project directory
|
|
68
|
+
try {
|
|
69
|
+
await promises_1.default.access(destDir, node_fs_1.default.constants.F_OK);
|
|
70
|
+
console.log(`Using existing directory: ${node_path_1.default.dirname(destDir)}\n`);
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
await promises_1.default.mkdir(destDir, { recursive: true });
|
|
74
|
+
console.log(`Created new directory: ${projectDir}\n`);
|
|
75
|
+
}
|
|
76
|
+
console.log('Generating application files...');
|
|
77
|
+
// Copy template contents into a new directory
|
|
78
|
+
const templateDir = node_path_1.default.join(__dirname, './templates/default');
|
|
79
|
+
await promises_1.default.cp(templateDir, destDir, { recursive: true });
|
|
80
|
+
// Define all variables used in template files with .tpl extension
|
|
81
|
+
const variables = {
|
|
82
|
+
NAME: name.charAt(0).toUpperCase() + name.slice(1),
|
|
83
|
+
LOWER_NAME: sanitizedName,
|
|
84
|
+
DESCRIPTION: description,
|
|
85
|
+
HOST: command.getOptionValue('host'),
|
|
86
|
+
PORT: command.getOptionValue('port'),
|
|
87
|
+
DEPENDENCIES: getNodeDependencies(command, runtime).join(',\n'),
|
|
88
|
+
DATABASE_URL: getDatabaseUrl(command, sanitizedName, 'dev'),
|
|
89
|
+
DATABASE_TEST_URL: getDatabaseUrl(command, sanitizedName, 'test'),
|
|
90
|
+
VERSION: pkg.version
|
|
91
|
+
};
|
|
92
|
+
// Process .tpl files: replace variables and remove .tpl extension
|
|
93
|
+
const templateFiles = await (0, glob_1.glob)('**/*.tpl', {
|
|
94
|
+
cwd: destDir,
|
|
95
|
+
absolute: true,
|
|
96
|
+
dot: true
|
|
97
|
+
});
|
|
98
|
+
for (const templateFile of templateFiles) {
|
|
99
|
+
let content = await promises_1.default.readFile(templateFile, 'utf8');
|
|
100
|
+
// Replace variables with values
|
|
101
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
102
|
+
content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
|
|
103
|
+
}
|
|
104
|
+
// Save output to new file and remove template file
|
|
105
|
+
const outputFile = templateFile.replace(/\.tpl$/, '');
|
|
106
|
+
await promises_1.default.writeFile(outputFile, content, 'utf8');
|
|
107
|
+
await promises_1.default.unlink(templateFile);
|
|
108
|
+
}
|
|
109
|
+
// Find all runtime-specific files and keep only those for current runtime
|
|
110
|
+
const runtimeFiles = await (0, glob_1.glob)(`**/*.{node,bun}`, {
|
|
111
|
+
cwd: destDir,
|
|
112
|
+
absolute: true,
|
|
113
|
+
dot: true
|
|
114
|
+
});
|
|
115
|
+
for (const runtimeFile of runtimeFiles) {
|
|
116
|
+
if (runtimeFile.endsWith(`.${runtime}`)) {
|
|
117
|
+
const outputFile = runtimeFile.replace(`.${runtime}`, '');
|
|
118
|
+
await promises_1.default.cp(runtimeFile, outputFile);
|
|
119
|
+
}
|
|
120
|
+
await promises_1.default.unlink(runtimeFile);
|
|
121
|
+
}
|
|
122
|
+
// Create test reports directory
|
|
123
|
+
await promises_1.default.mkdir(node_path_1.default.join(destDir, 'reports'));
|
|
124
|
+
// Add instructions for AI Agents and skill files
|
|
125
|
+
const agent = command.getOptionValue('agent');
|
|
126
|
+
if (agent !== 'none') {
|
|
127
|
+
let agentsDir;
|
|
128
|
+
if (['claude', 'junie', 'kiro', 'pi', 'opencode'].includes(agent)) {
|
|
129
|
+
agentsDir = `.${agent}`;
|
|
130
|
+
}
|
|
131
|
+
else if (agent === 'copilot') {
|
|
132
|
+
agentsDir = '.github';
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
agentsDir = '.agents';
|
|
136
|
+
}
|
|
137
|
+
const guidelinesFileName = agent === 'claude' ? 'CLAUDE.md' : 'AGENTS.md';
|
|
138
|
+
// Copy skill and referenced files
|
|
139
|
+
const skillDir = node_path_1.default.join(__dirname, 'skill');
|
|
140
|
+
const projectSkillPath = node_path_1.default.join(agentsDir, 'skills', 'appweaver');
|
|
141
|
+
const projectSkillDir = node_path_1.default.join(destDir, projectSkillPath);
|
|
142
|
+
await promises_1.default.cp(skillDir, projectSkillDir, {
|
|
143
|
+
recursive: true
|
|
144
|
+
});
|
|
145
|
+
// Move the project guidelines file to the project root
|
|
146
|
+
const guidelinesSkillPath = node_path_1.default.join(projectSkillDir, 'GUIDELINES.md');
|
|
147
|
+
const guidelinesFilePath = node_path_1.default.join(destDir, guidelinesFileName);
|
|
148
|
+
await promises_1.default.rename(guidelinesSkillPath, guidelinesFilePath);
|
|
149
|
+
// Update project guidelines reference paths to point in skills directory
|
|
150
|
+
const guidelinesContents = await promises_1.default.readFile(guidelinesFilePath, 'utf8');
|
|
151
|
+
const referencesPath = node_path_1.default
|
|
152
|
+
.join(projectSkillPath, 'references')
|
|
153
|
+
.replace(/\\/g, '/');
|
|
154
|
+
const updatedGuidelinesContent = guidelinesContents.replace(/(\[.+]\()references\/(.+\))/g, `$1${referencesPath}/$2`);
|
|
155
|
+
await promises_1.default.writeFile(guidelinesFilePath, updatedGuidelinesContent, {
|
|
156
|
+
encoding: 'utf8'
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
console.log(`Done\n`);
|
|
160
|
+
if (command.getOptionValue('skipInstall')) {
|
|
161
|
+
console.log(`${name} created successfully!`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
console.log(`Installing dependencies...`);
|
|
165
|
+
await runProcess(packageManager, ['install', '--no-audit', '--no-fund', '--loglevel=error'], { destDir });
|
|
166
|
+
console.log(`Done\n`);
|
|
167
|
+
console.log(`Configuring application...`);
|
|
168
|
+
await runProcess(packageManager, ['run', 'generate'], { destDir });
|
|
169
|
+
console.log(`Done\n`);
|
|
170
|
+
console.log(`${name} created successfully!`);
|
|
171
|
+
})
|
|
172
|
+
.parse();
|
|
173
|
+
function getNodeDependencies(command, runtime) {
|
|
174
|
+
const dependencies = [];
|
|
175
|
+
const adapters = {
|
|
176
|
+
sqlite: runtime === 'bun'
|
|
177
|
+
? `"@prisma/adapter-libsql": "${prismaVersion}"`
|
|
178
|
+
: `"@prisma/adapter-better-sqlite3": "${prismaVersion}"`,
|
|
179
|
+
postgresql: `"@prisma/adapter-pg": "${prismaVersion}"`,
|
|
180
|
+
mysql: `"@prisma/adapter-mariadb": "${prismaVersion}"`,
|
|
181
|
+
sqlserver: `"@prisma/adapter-mssql": "${prismaVersion}"`
|
|
182
|
+
};
|
|
183
|
+
const database = command.getOptionValue('database');
|
|
184
|
+
const databaseDependency = adapters[database.toLowerCase()];
|
|
185
|
+
if (!databaseDependency) {
|
|
186
|
+
console.error(`Invalid database type: ${database}`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
dependencies.push(databaseDependency);
|
|
190
|
+
dependencies.push(`"@prisma/client": "${prismaVersion}"`);
|
|
191
|
+
dependencies.push(`"prisma": "${prismaVersion}"`);
|
|
192
|
+
if (!command.getOptionValue('noQueue')) {
|
|
193
|
+
dependencies.push('"bullmq": "5.70.1"');
|
|
194
|
+
}
|
|
195
|
+
if (!command.getOptionValue('noCron')) {
|
|
196
|
+
dependencies.push('"cron": "4.4.0"');
|
|
197
|
+
}
|
|
198
|
+
if (!command.getOptionValue('noRedis')) {
|
|
199
|
+
dependencies.push('"ioredis": "5.9.3"');
|
|
200
|
+
}
|
|
201
|
+
if (!command.getOptionValue('noMailer')) {
|
|
202
|
+
dependencies.push('"nodemailer": "8.0.5"');
|
|
203
|
+
}
|
|
204
|
+
return dependencies.sort().map((d) => ` ${d}`);
|
|
205
|
+
}
|
|
206
|
+
function getDatabaseUrl(command, name, mode) {
|
|
207
|
+
const dbName = mode === 'test' ? `${name}-test` : name;
|
|
208
|
+
const urls = {
|
|
209
|
+
sqlite: `file:./${mode === 'test' ? 'temp/' : ''}${dbName}.db`,
|
|
210
|
+
postgresql: `postgresql://${name}:${name}@localhost:5432/${dbName}?schema=public`,
|
|
211
|
+
mysql: `mysql://${name}:${name}@localhost:3306/${dbName}`,
|
|
212
|
+
sqlserver: `sqlserver://localhost:1433;database=${dbName};user=${name};password=${name};trustServerCertificate=true`
|
|
213
|
+
};
|
|
214
|
+
const database = command.getOptionValue('database');
|
|
215
|
+
const databaseUrl = urls[database.toLowerCase()];
|
|
216
|
+
if (!databaseUrl) {
|
|
217
|
+
console.error(`Invalid database type: ${database}`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
return databaseUrl;
|
|
221
|
+
}
|
|
222
|
+
function runProcess(cmd, args = [], params = {}) {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
const { destDir, quiet } = params;
|
|
225
|
+
const command = args.length > 0 ? `${cmd} ${args.join(' ')}` : cmd;
|
|
226
|
+
const child = (0, node_child_process_1.spawn)(command, {
|
|
227
|
+
stdio: quiet ? 'ignore' : 'inherit',
|
|
228
|
+
shell: true,
|
|
229
|
+
cwd: destDir
|
|
230
|
+
});
|
|
231
|
+
child.on('error', reject);
|
|
232
|
+
child.on('close', (code) => {
|
|
233
|
+
resolve(code);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function parseDatabaseType(value) {
|
|
238
|
+
const lowerDbType = value.toLowerCase();
|
|
239
|
+
if (!dbTypes.includes(lowerDbType)) {
|
|
240
|
+
throw new commander_1.InvalidOptionArgumentError(`Must be one of following: ${dbTypes.join(', ')}.`);
|
|
241
|
+
}
|
|
242
|
+
return lowerDbType;
|
|
243
|
+
}
|
|
244
|
+
function parsePortNumber(value) {
|
|
245
|
+
const int = parseInt(value, 10);
|
|
246
|
+
if (isNaN(int) || int < 0 || int > 65535) {
|
|
247
|
+
throw new commander_1.InvalidOptionArgumentError('Must be an integer between 0 and 65535.');
|
|
248
|
+
}
|
|
249
|
+
return int;
|
|
250
|
+
}
|
|
251
|
+
function parseHostname(value) {
|
|
252
|
+
try {
|
|
253
|
+
new URL(`http://${value}`);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
throw new commander_1.InvalidOptionArgumentError('Must be a valid hostname or IP address.');
|
|
257
|
+
}
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
function parseAgentType(value) {
|
|
261
|
+
const lowerAgentType = value.toLowerCase();
|
|
262
|
+
if (!agentTypes.includes(lowerAgentType)) {
|
|
263
|
+
throw new commander_1.InvalidOptionArgumentError(`Must be one of following: ${agentTypes.join(', ')}.`);
|
|
264
|
+
}
|
|
265
|
+
return lowerAgentType;
|
|
266
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@appweaver/create-weaver-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Appweaver - simple, fast, and reliable web application builder tool (@create-weaver-app)",
|
|
5
|
+
"author": "Luka Matosevic",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"main": "./create-weaver-app.js",
|
|
8
|
+
"types": "./create-weaver-app.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"create-weaver-app": "./create-weaver-app.js"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/lmatosevic/appweaver.git",
|
|
15
|
+
"directory": "packages/create-weaver-app"
|
|
16
|
+
},
|
|
17
|
+
"maintainers": [
|
|
18
|
+
{
|
|
19
|
+
"name": "Luka Matosevic",
|
|
20
|
+
"email": "lukamatosevic5@gmail.com",
|
|
21
|
+
"web": "https://lukamatosevic.com"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">= 20"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"create-weaver-app": "node ./dist/create-weaver-app.js"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "14.0.3",
|
|
35
|
+
"glob": "13.0.1"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Appweaver Guidelines
|
|
2
|
+
|
|
3
|
+
Appweaver is a TypeScript/Node.js library for building web applications. Built on Fastify (HTTP) and Prisma (ORM), it
|
|
4
|
+
provides factory methods for creating resource models, services, policies, and routes with sensible defaults.
|
|
5
|
+
|
|
6
|
+
## Project structure
|
|
7
|
+
|
|
8
|
+
- `database/` - migrations, seeders, generated Prisma client
|
|
9
|
+
- `dist/` - transpiled JavaScript output
|
|
10
|
+
- `public/` - static files (if enabled)
|
|
11
|
+
- `src/features/` - application logic (vertical slice architecture)
|
|
12
|
+
- `src/resources/` - resources (models, services, policies, routes)
|
|
13
|
+
- `src/types/` - generated and manual types
|
|
14
|
+
- `src/main.ts` - application entrypoint
|
|
15
|
+
- `test/e2e/` - end-to-end tests
|
|
16
|
+
- `test/unit/` - unit tests
|
|
17
|
+
- `.env` / `.env.{env}` - environment variable overrides (optional)
|
|
18
|
+
- `appweaver.json` / `appweaver.{env}.json` - central configuration
|
|
19
|
+
- `Dockerfile` - Docker image definition
|
|
20
|
+
|
|
21
|
+
**IMPORTANT:** `{env}` is controlled by `NODE_ENV` evironment variable.
|
|
22
|
+
|
|
23
|
+
## Application entrypoint
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// src/main.ts
|
|
27
|
+
import { createApp } from '@appweaver/core';
|
|
28
|
+
import { logger } from '@appweaver/common';
|
|
29
|
+
|
|
30
|
+
createApp().catch((err) => logger.error(err));
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Creating resources
|
|
34
|
+
|
|
35
|
+
Resources are the core building blocks. There are four types: **model**, **service**, **routes**, and **policy**.
|
|
36
|
+
Exported resources are loaded automatically on application start.
|
|
37
|
+
|
|
38
|
+
Dependency chain: **model** → **service** → **routes** → **policy**
|
|
39
|
+
|
|
40
|
+
Only a model is required. If a service exists, a model must exist. If routes exist, a service must exist. Policy is
|
|
41
|
+
independent.
|
|
42
|
+
|
|
43
|
+
**DOS:**
|
|
44
|
+
|
|
45
|
+
- Use default configuration values whenever possible
|
|
46
|
+
- Rely on library defaults for `omit`/`pick`, and `input`/`output` settings
|
|
47
|
+
- Use default `mimeType` and `namePattern` patterns in file configurations unless specifically requested
|
|
48
|
+
- Prefer storing configuration in JSON file (`appweaver.json`) over environment (`.env`) file, but prefer it for secrets
|
|
49
|
+
- Always create all four resource configs (model, service, routes, and policy) unless specified otherwise
|
|
50
|
+
|
|
51
|
+
**DON'TS:**
|
|
52
|
+
|
|
53
|
+
- Don't explicitly set default values in configuration unless specifically requested
|
|
54
|
+
- Don't override `omit`/`pick` for `read`, `create` and `update` settings unnecessarily
|
|
55
|
+
- Don't specify `input`/`output` configurations if defaults suffice
|
|
56
|
+
- Don't modify file's `mimeType` and `namePattern` patterns unless specifically instructed
|
|
57
|
+
- Don't customize index arrays without an explicit requirement
|
|
58
|
+
|
|
59
|
+
### Model
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// src/resources/product/model.ts
|
|
63
|
+
import { createModel } from '@appweaver/core';
|
|
64
|
+
|
|
65
|
+
export default createModel({
|
|
66
|
+
name: 'Product',
|
|
67
|
+
scalars: {
|
|
68
|
+
title: { type: 'string', minLength: 1, maxLength: 200 },
|
|
69
|
+
price: { type: 'float', minimum: 0 },
|
|
70
|
+
status: { type: 'enum', default: 'Draft', values: ['Draft', 'Active', 'Sold'] },
|
|
71
|
+
description: { type: 'string', required: false },
|
|
72
|
+
lastViewedAt: { type: 'dateTime', defaultGenerator: 'now()' },
|
|
73
|
+
enabled: { type: 'boolean', default: true }
|
|
74
|
+
},
|
|
75
|
+
relations: {
|
|
76
|
+
category: { model: 'Category', mappedBy: 'products', owner: true, output: { type: 'always' } }
|
|
77
|
+
},
|
|
78
|
+
files: {
|
|
79
|
+
photo: { mimeType: 'image/*', maxSize: '2 MB' }
|
|
80
|
+
},
|
|
81
|
+
create: { omit: ['status'] },
|
|
82
|
+
update: { pick: ['title', 'price', 'status', 'description'] },
|
|
83
|
+
index: ['title']
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Service
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// src/resources/product/service.ts
|
|
91
|
+
import { createService } from '@appweaver/core';
|
|
92
|
+
|
|
93
|
+
export default createService({
|
|
94
|
+
modelName: 'Product',
|
|
95
|
+
afterCreate: (resource) => {
|
|
96
|
+
console.log('Product created:', resource.id);
|
|
97
|
+
},
|
|
98
|
+
textSearch: {
|
|
99
|
+
title: { contains: '{input}', mode: 'insensitive' }
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Routes
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
// src/resources/product/routes.ts
|
|
108
|
+
import { createRoutes } from '@appweaver/core';
|
|
109
|
+
|
|
110
|
+
export default createRoutes({
|
|
111
|
+
modelName: 'Product',
|
|
112
|
+
find: { cache: true, roles: ['Admin', 'User'], rateLimit: { max: 100 } },
|
|
113
|
+
query: { cacheTTL: 5000 },
|
|
114
|
+
create: { permissions: ['product:create'] },
|
|
115
|
+
delete: { exclude: true }
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Policy
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// src/resources/product/policy.ts
|
|
123
|
+
import { createPolicy } from '@appweaver/core';
|
|
124
|
+
|
|
125
|
+
export default createPolicy({
|
|
126
|
+
modelName: 'Product',
|
|
127
|
+
checkAccess: (action, resource) => resource.status === 'Draft',
|
|
128
|
+
readRestrictions: (action, resource) => {
|
|
129
|
+
enabled: true;
|
|
130
|
+
},
|
|
131
|
+
files: {
|
|
132
|
+
photo: { accessType: 'public' }
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Auth model and service
|
|
138
|
+
|
|
139
|
+
Use `createAuthModel` and `createAuthService` for authenticatable users. They must be used together.
|
|
140
|
+
|
|
141
|
+
`createAuthModel` adds: `email`, `passwordHash`, `verifiedEmail`, `twoFactorAuth`, `enabled`, `logoutAt` scalars; a
|
|
142
|
+
virtual `password` field; a `roles` relation; and optional `apiKeys` relation.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// src/resources/user/model.ts
|
|
146
|
+
import { createAuthModel } from '@appweaver/core';
|
|
147
|
+
|
|
148
|
+
export default createAuthModel({
|
|
149
|
+
name: 'User',
|
|
150
|
+
scalars: { name: { type: 'string', maxLength: 100 } },
|
|
151
|
+
files: { avatar: { mimeType: 'image/(png|jpeg|gif)', maxSize: '2 MB' } }
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// src/resources/user/service.ts
|
|
157
|
+
import { createAuthService } from '@appweaver/core';
|
|
158
|
+
|
|
159
|
+
export default createAuthService({
|
|
160
|
+
modelName: 'User',
|
|
161
|
+
registrationData: (_, email, password) => ({ email, password, roles: [1, 2] })
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Custom routes, models, and plugins
|
|
166
|
+
|
|
167
|
+
### Custom route
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// src/features/custom-route.ts
|
|
171
|
+
import { registerRoute, Router } from '@appweaver/core';
|
|
172
|
+
import { Type } from '@sinclair/typebox';
|
|
173
|
+
|
|
174
|
+
registerRoute(
|
|
175
|
+
async function (router: Router) {
|
|
176
|
+
router.get('/search-result', {
|
|
177
|
+
schema: { summary: 'Search result', response: { 200: Type.Ref('SearchResult') } },
|
|
178
|
+
handler: async () => ({ message: 'Hello, world!' })
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
{ public: true, cacheTTL: 15000 }
|
|
182
|
+
);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Custom model
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
// src/features/custom-model.ts
|
|
189
|
+
import { registerModel } from '@appweaver/core';
|
|
190
|
+
import { Type } from '@sinclair/typebox';
|
|
191
|
+
|
|
192
|
+
registerModel(
|
|
193
|
+
Type.Object(
|
|
194
|
+
{ id: Type.Number(), title: Type.String(), score: Type.Number({ minimum: 0, maximum: 1 }) },
|
|
195
|
+
{ $id: 'SearchResult' }
|
|
196
|
+
)
|
|
197
|
+
);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Plugin
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
// src/plugins/audit-log.ts
|
|
204
|
+
import { registerPlugin } from '@appweaver/core';
|
|
205
|
+
|
|
206
|
+
registerPlugin('audit-log', async (server) => {
|
|
207
|
+
server.addHook('onResponse', async (request, reply) => {
|
|
208
|
+
console.log(`${request.method} ${request.url} → ${reply.statusCode}`);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Dependency injection
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { Cache } from '@appweaver/common';
|
|
217
|
+
import { define, inject, loadProvider } from '@appweaver/core';
|
|
218
|
+
|
|
219
|
+
define(RedisCacheService, Cache); // register class under abstract token
|
|
220
|
+
define('https://api.example.com', 'ApiBaseUrl'); // register plain value
|
|
221
|
+
|
|
222
|
+
const cache = inject(Cache); // resolve singleton
|
|
223
|
+
const url = inject<string>('ApiBaseUrl'); // resolve by string token
|
|
224
|
+
|
|
225
|
+
// Dynamic provider loading (typical in main.ts)
|
|
226
|
+
loadProvider(__dirname, config.CACHE_PROVIDER, Cache);
|
|
227
|
+
loadProvider(__dirname, config.MAILER_PROVIDER, Mailer, false); // optional
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Seeders
|
|
231
|
+
|
|
232
|
+
Seeder files export async functions and run in alphabetical order. Prefix filenames with ordinal numbers.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
// database/seeders/001-create-admin-user.ts
|
|
236
|
+
import { hashPassword } from '@appweaver/core';
|
|
237
|
+
import { db } from '@db/client';
|
|
238
|
+
|
|
239
|
+
export async function createAdminUser(): Promise<void> {
|
|
240
|
+
await db.user.create({
|
|
241
|
+
data: {
|
|
242
|
+
firstName: 'Admin',
|
|
243
|
+
lastName: 'Admin',
|
|
244
|
+
email: 'admin@appweaver.com',
|
|
245
|
+
roles: {
|
|
246
|
+
connectOrCreate: [
|
|
247
|
+
{
|
|
248
|
+
where: { name: 'Admin' },
|
|
249
|
+
create: {
|
|
250
|
+
name: 'Admin',
|
|
251
|
+
permissions: {
|
|
252
|
+
connectOrCreate: [
|
|
253
|
+
{ where: { name: '*.read' }, create: { name: '*.read' } },
|
|
254
|
+
{ where: { name: '*.write' }, create: { name: '*.write' } }
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Common commands
|
|
267
|
+
|
|
268
|
+
| Command | Description |
|
|
269
|
+
|-------------------------------|-------------------------------------------|
|
|
270
|
+
| `npm run generate` | Generate TypeScript types + Prisma schema |
|
|
271
|
+
| `npm run build` | Build the application |
|
|
272
|
+
| `npm run start` | Start in production mode |
|
|
273
|
+
| `npm run dev` | Start in development (watch) mode |
|
|
274
|
+
| `npm run seed` | Seed the database |
|
|
275
|
+
| `npm run migrate` | Apply pending database migrations |
|
|
276
|
+
| `npm run test` | Run unit tests |
|
|
277
|
+
| `npm run e2e` | Run end-to-end tests |
|
|
278
|
+
| `npm run format` | Format code with Prettier |
|
|
279
|
+
| `npm run lint` | Lint code with ESLint |
|
|
280
|
+
| `weaver migration new <name>` | Create a new database migration |
|
|
281
|
+
| `weaver update` | Update all @appweaver/* packages |
|
|
282
|
+
| `weaver openapi` | Generate OpenAPI specification |
|
|
283
|
+
|
|
284
|
+
## References
|
|
285
|
+
|
|
286
|
+
- Application CLI (weaver): [cli.md](references/cli.md)
|
|
287
|
+
- Application configuration: [configuration.md](references/configuration.md)
|
|
288
|
+
- Application resources: [resources.md](references/resources.md)
|
|
289
|
+
- Dependency injection: [dependency-injection.md](references/dependency-injection.md)
|
|
290
|
+
- Security details: [security.md](references/security.md)
|
|
291
|
+
- Storage & File management: [storage.md](references/storage.md)
|
|
292
|
+
- Database & Migrations: [database.md](references/database.md)
|
|
293
|
+
- Events & Hooks: [events.md](references/events.md)
|
|
294
|
+
- Cache management: [cache.md](references/cache.md)
|
|
295
|
+
- Queue jobs: [queue.md](references/queue.md)
|
|
296
|
+
- Scheduling jobs: [scheduler.md](references/scheduler.md)
|
|
297
|
+
- Sending emails: [mailer.md](references/mailer.md)
|
|
298
|
+
- Generating an HTTP client for using API: [client.md](references/client.md)
|