@slingr/cli 0.0.2 → 0.0.4
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.txt +202 -0
- package/README.md +490 -319
- package/bin/dev.cmd +2 -2
- package/bin/dev.js +5 -5
- package/bin/run.cmd +2 -2
- package/bin/run.js +4 -4
- package/bin/slingr +1 -0
- package/dist/commands/build.d.ts +20 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +206 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/create-app.d.ts +0 -1
- package/dist/commands/create-app.d.ts.map +1 -1
- package/dist/commands/create-app.js +38 -57
- package/dist/commands/create-app.js.map +1 -1
- package/dist/commands/debug.d.ts +28 -0
- package/dist/commands/debug.d.ts.map +1 -0
- package/dist/commands/debug.js +474 -0
- package/dist/commands/debug.js.map +1 -0
- package/dist/commands/ds.d.ts +14 -1
- package/dist/commands/ds.d.ts.map +1 -1
- package/dist/commands/ds.js +450 -121
- package/dist/commands/ds.js.map +1 -1
- package/dist/commands/gql.d.ts +1 -1
- package/dist/commands/gql.d.ts.map +1 -1
- package/dist/commands/gql.js +190 -184
- package/dist/commands/gql.js.map +1 -1
- package/dist/commands/infra/down.d.ts.map +1 -1
- package/dist/commands/infra/down.js +8 -7
- package/dist/commands/infra/down.js.map +1 -1
- package/dist/commands/infra/up.d.ts.map +1 -1
- package/dist/commands/infra/up.js +8 -7
- package/dist/commands/infra/up.js.map +1 -1
- package/dist/commands/infra/update.d.ts +1 -0
- package/dist/commands/infra/update.d.ts.map +1 -1
- package/dist/commands/infra/update.js +33 -69
- package/dist/commands/infra/update.js.map +1 -1
- package/dist/commands/run.d.ts +29 -2
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +628 -130
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +34 -71
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/sync-metadata.d.ts +15 -0
- package/dist/commands/sync-metadata.d.ts.map +1 -0
- package/dist/commands/sync-metadata.js +225 -0
- package/dist/commands/sync-metadata.js.map +1 -0
- package/dist/commands/users.d.ts +30 -0
- package/dist/commands/users.d.ts.map +1 -0
- package/dist/commands/users.js +472 -0
- package/dist/commands/users.js.map +1 -0
- package/dist/commands/views.d.ts +11 -0
- package/dist/commands/views.d.ts.map +1 -0
- package/dist/commands/views.js +73 -0
- package/dist/commands/views.js.map +1 -0
- package/dist/projectStructure.d.ts +2 -2
- package/dist/projectStructure.d.ts.map +1 -1
- package/dist/projectStructure.js +281 -69
- package/dist/projectStructure.js.map +1 -1
- package/dist/scripts/generate-metadata.d.ts +13 -0
- package/dist/scripts/generate-metadata.d.ts.map +1 -0
- package/dist/scripts/generate-metadata.js +412 -0
- package/dist/scripts/generate-metadata.js.map +1 -0
- package/dist/scripts/generate-metadata.ts +498 -0
- package/dist/scripts/generate-schema.d.ts +1 -1
- package/dist/scripts/generate-schema.js +168 -74
- package/dist/scripts/generate-schema.js.map +1 -1
- package/dist/scripts/generate-schema.ts +258 -143
- package/dist/templates/.env.template +23 -0
- package/dist/templates/.firebaserc.template +5 -0
- package/dist/templates/.github/copilot-instructions.md.template +652 -17
- package/dist/templates/backend/Dockerfile.template +30 -0
- package/dist/templates/config/datasource.ts.template +12 -9
- package/dist/templates/config/jest.config.ts +30 -30
- package/dist/templates/config/jest.setup.ts +1 -1
- package/dist/templates/config/tsconfig.json.template +50 -29
- package/dist/templates/dataSources/mysql.ts.template +16 -13
- package/dist/templates/dataSources/postgres.ts.template +15 -13
- package/dist/templates/dataset-generator-script.ts.template +139 -139
- package/dist/templates/datasets/mysql-default/.slingr-schema.json.template +5 -0
- package/dist/templates/datasets/mysql-default/Address.jsonl.template +3 -3
- package/dist/templates/datasets/mysql-default/App.jsonl.template +4 -4
- package/dist/templates/datasets/mysql-default/Company.jsonl.template +3 -3
- package/dist/templates/datasets/mysql-default/Person.jsonl.template +2 -2
- package/dist/templates/datasets/mysql-default/User.jsonl.template +1 -0
- package/dist/templates/datasets/mysql-default/instructions.md.template +1 -0
- package/dist/templates/datasets/postgres-default/.slingr-schema.json.template +5 -0
- package/dist/templates/datasets/postgres-default/Address.jsonl.template +3 -3
- package/dist/templates/datasets/postgres-default/App.jsonl.template +4 -4
- package/dist/templates/datasets/postgres-default/Company.jsonl.template +3 -3
- package/dist/templates/datasets/postgres-default/Person.jsonl.template +2 -2
- package/dist/templates/datasets/postgres-default/User.jsonl.template +1 -0
- package/dist/templates/datasets/postgres-default/instructions.md.template +1 -0
- package/dist/templates/docker-compose.prod-test.yml.template +32 -0
- package/dist/templates/docker-compose.yml.template +24 -0
- package/dist/templates/docs/app-description.md.template +33 -33
- package/dist/templates/firebase.json.template +68 -0
- package/dist/templates/frontend/.umirc.ts.template +23 -0
- package/dist/templates/frontend/package.json.template +45 -0
- package/dist/templates/frontend/public/config.json +6 -0
- package/dist/templates/frontend/public/logo.svg +6 -0
- package/dist/templates/frontend/src/app.tsx.template +44 -0
- package/dist/templates/frontend/src/global.less.template +117 -0
- package/dist/templates/frontend/src/layouts/MainLayout.tsx.template +75 -0
- package/dist/templates/frontend/src/types/graphql-augmentation.d.ts.template +44 -0
- package/dist/templates/frontend/src/views/customViews/user/UserCreateView.tsx.template +18 -0
- package/dist/templates/frontend/src/views/customViews/user/UserEditView.tsx.template +29 -0
- package/dist/templates/frontend/src/views/customViews/user/UserReadView.tsx.template +24 -0
- package/dist/templates/frontend/src/views/customViews/user/UserTableView.tsx.template +38 -0
- package/dist/templates/frontend/src/views/customViews/welcome.tsx.template +34 -0
- package/dist/templates/frontend/tsconfig.json.template +50 -0
- package/dist/templates/gql/codegen.yml.template +25 -25
- package/dist/templates/gql/index.ts.template +17 -24
- package/dist/templates/gql/operations.graphql.template +30 -30
- package/dist/templates/ops/README.md.template +1045 -0
- package/dist/templates/ops/cloudbuild.yaml.template +161 -0
- package/dist/templates/ops/scripts/_utils.js.template +217 -0
- package/dist/templates/ops/scripts/deploy.js.template +145 -0
- package/dist/templates/ops/scripts/setup-gcp.js.template +330 -0
- package/dist/templates/ops/scripts/setup-secrets.js.template +76 -0
- package/dist/templates/ops/scripts/test-prod-local.js.template +49 -0
- package/dist/templates/package.json.template +50 -38
- package/dist/templates/pnpm-workspace.yaml.template +3 -0
- package/dist/templates/prompt-analysis.md.template +110 -110
- package/dist/templates/prompt-script-generation.md.template +258 -258
- package/dist/templates/src/Address.ts.template +28 -31
- package/dist/templates/src/App.ts.template +17 -61
- package/dist/templates/src/Company.ts.template +41 -47
- package/dist/templates/src/Models.test.ts.template +654 -654
- package/dist/templates/src/Person.test.ts.template +289 -289
- package/dist/templates/src/Person.ts.template +90 -105
- package/dist/templates/src/actions/index.ts.template +11 -11
- package/dist/templates/src/auth/permissions.ts.template +34 -0
- package/dist/templates/src/data/App.ts.template +48 -0
- package/dist/templates/src/data/User.ts.template +35 -0
- package/dist/templates/src/types/gql.d.ts.template +17 -17
- package/dist/templates/vscode/extensions.json +4 -3
- package/dist/templates/vscode/settings.json +17 -11
- package/dist/templates/workspace-package.json.template +21 -0
- package/dist/utils/buildCache.d.ts +12 -0
- package/dist/utils/buildCache.d.ts.map +1 -0
- package/dist/utils/buildCache.js +102 -0
- package/dist/utils/buildCache.js.map +1 -0
- package/dist/utils/checkFramework.d.ts +27 -0
- package/dist/utils/checkFramework.d.ts.map +1 -0
- package/dist/utils/checkFramework.js +104 -0
- package/dist/utils/checkFramework.js.map +1 -0
- package/dist/utils/datasourceParser.d.ts +11 -0
- package/dist/utils/datasourceParser.d.ts.map +1 -1
- package/dist/utils/datasourceParser.js +154 -56
- package/dist/utils/datasourceParser.js.map +1 -1
- package/dist/utils/dockerManager.d.ts +25 -0
- package/dist/utils/dockerManager.d.ts.map +1 -0
- package/dist/utils/dockerManager.js +281 -0
- package/dist/utils/dockerManager.js.map +1 -0
- package/dist/utils/infraFileParser.d.ts +26 -0
- package/dist/utils/infraFileParser.d.ts.map +1 -0
- package/dist/utils/infraFileParser.js +75 -0
- package/dist/utils/infraFileParser.js.map +1 -0
- package/dist/utils/jsonlLoader.d.ts +91 -12
- package/dist/utils/jsonlLoader.d.ts.map +1 -1
- package/dist/utils/jsonlLoader.js +674 -63
- package/dist/utils/jsonlLoader.js.map +1 -1
- package/dist/utils/model-analyzer.d.ts.map +1 -1
- package/dist/utils/model-analyzer.js +67 -13
- package/dist/utils/model-analyzer.js.map +1 -1
- package/dist/utils/userManagement.d.ts +57 -0
- package/dist/utils/userManagement.d.ts.map +1 -0
- package/dist/utils/userManagement.js +288 -0
- package/dist/utils/userManagement.js.map +1 -0
- package/dist/utils/viewsGenerator.d.ts +15 -0
- package/dist/utils/viewsGenerator.d.ts.map +1 -0
- package/dist/utils/viewsGenerator.js +311 -0
- package/dist/utils/viewsGenerator.js.map +1 -0
- package/oclif.manifest.json +445 -20
- package/package.json +29 -27
- package/src/templates/.env.template +23 -0
- package/src/templates/.firebaserc.template +5 -0
- package/src/templates/.github/copilot-instructions.md.template +652 -17
- package/src/templates/backend/Dockerfile.template +30 -0
- package/src/templates/config/datasource.ts.template +12 -9
- package/src/templates/config/jest.config.ts +30 -30
- package/src/templates/config/jest.setup.ts +1 -1
- package/src/templates/config/tsconfig.json.template +50 -29
- package/src/templates/dataSources/mysql.ts.template +16 -13
- package/src/templates/dataSources/postgres.ts.template +15 -13
- package/src/templates/dataset-generator-script.ts.template +139 -139
- package/src/templates/datasets/mysql-default/.slingr-schema.json.template +5 -0
- package/src/templates/datasets/mysql-default/Address.jsonl.template +3 -3
- package/src/templates/datasets/mysql-default/App.jsonl.template +4 -4
- package/src/templates/datasets/mysql-default/Company.jsonl.template +3 -3
- package/src/templates/datasets/mysql-default/Person.jsonl.template +2 -2
- package/src/templates/datasets/mysql-default/User.jsonl.template +1 -0
- package/src/templates/datasets/mysql-default/instructions.md.template +1 -0
- package/src/templates/datasets/postgres-default/.slingr-schema.json.template +5 -0
- package/src/templates/datasets/postgres-default/Address.jsonl.template +3 -3
- package/src/templates/datasets/postgres-default/App.jsonl.template +4 -4
- package/src/templates/datasets/postgres-default/Company.jsonl.template +3 -3
- package/src/templates/datasets/postgres-default/Person.jsonl.template +2 -2
- package/src/templates/datasets/postgres-default/User.jsonl.template +1 -0
- package/src/templates/datasets/postgres-default/instructions.md.template +1 -0
- package/src/templates/docker-compose.prod-test.yml.template +32 -0
- package/src/templates/docker-compose.yml.template +24 -0
- package/src/templates/docs/app-description.md.template +33 -33
- package/src/templates/firebase.json.template +68 -0
- package/src/templates/frontend/.umirc.ts.template +23 -0
- package/src/templates/frontend/package.json.template +45 -0
- package/src/templates/frontend/public/config.json +6 -0
- package/src/templates/frontend/public/logo.svg +6 -0
- package/src/templates/frontend/src/app.tsx.template +44 -0
- package/src/templates/frontend/src/global.less.template +117 -0
- package/src/templates/frontend/src/layouts/MainLayout.tsx.template +75 -0
- package/src/templates/frontend/src/types/graphql-augmentation.d.ts.template +44 -0
- package/src/templates/frontend/src/views/customViews/user/UserCreateView.tsx.template +18 -0
- package/src/templates/frontend/src/views/customViews/user/UserEditView.tsx.template +29 -0
- package/src/templates/frontend/src/views/customViews/user/UserReadView.tsx.template +24 -0
- package/src/templates/frontend/src/views/customViews/user/UserTableView.tsx.template +38 -0
- package/src/templates/frontend/src/views/customViews/welcome.tsx.template +34 -0
- package/src/templates/frontend/tsconfig.json.template +50 -0
- package/src/templates/gql/codegen.yml.template +25 -25
- package/src/templates/gql/index.ts.template +17 -24
- package/src/templates/gql/operations.graphql.template +30 -30
- package/src/templates/ops/README.md.template +1045 -0
- package/src/templates/ops/cloudbuild.yaml.template +161 -0
- package/src/templates/ops/scripts/_utils.js.template +217 -0
- package/src/templates/ops/scripts/deploy.js.template +145 -0
- package/src/templates/ops/scripts/setup-gcp.js.template +330 -0
- package/src/templates/ops/scripts/setup-secrets.js.template +76 -0
- package/src/templates/ops/scripts/test-prod-local.js.template +49 -0
- package/src/templates/package.json.template +50 -38
- package/src/templates/pnpm-workspace.yaml.template +3 -0
- package/src/templates/prompt-analysis.md.template +110 -110
- package/src/templates/prompt-script-generation.md.template +258 -258
- package/src/templates/src/Address.ts.template +28 -31
- package/src/templates/src/App.ts.template +17 -61
- package/src/templates/src/Company.ts.template +41 -47
- package/src/templates/src/Models.test.ts.template +654 -654
- package/src/templates/src/Person.test.ts.template +289 -289
- package/src/templates/src/Person.ts.template +90 -105
- package/src/templates/src/actions/index.ts.template +11 -11
- package/src/templates/src/auth/permissions.ts.template +34 -0
- package/src/templates/src/data/App.ts.template +48 -0
- package/src/templates/src/data/User.ts.template +35 -0
- package/src/templates/src/types/gql.d.ts.template +17 -17
- package/src/templates/vscode/extensions.json +4 -3
- package/src/templates/vscode/settings.json +17 -11
- package/src/templates/workspace-package.json.template +21 -0
- package/dist/templates/src/index.ts +0 -66
- package/src/templates/src/index.ts +0 -66
|
@@ -1,17 +1,652 @@
|
|
|
1
|
-
# GitHub Copilot Instructions for {{APP_NAME}}
|
|
2
|
-
|
|
3
|
-
This is a {{APP_TYPE}} application built with Slingr.
|
|
4
|
-
|
|
5
|
-
## Project Description
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- Use
|
|
1
|
+
# GitHub Copilot Instructions for {{APP_NAME}}
|
|
2
|
+
|
|
3
|
+
This is a {{APP_TYPE}} application built with Slingr.
|
|
4
|
+
|
|
5
|
+
## Project Description
|
|
6
|
+
|
|
7
|
+
{{DESCRIPTION}}
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
- Backend: {{HAS_BACKEND}}
|
|
12
|
+
- Frontend: {{HAS_FRONTEND}}
|
|
13
|
+
- Database: {{DB_TYPE}}
|
|
14
|
+
|
|
15
|
+
## Development Guidelines
|
|
16
|
+
|
|
17
|
+
- Use TypeScript for all code
|
|
18
|
+
- Follow Slingr conventions and patterns
|
|
19
|
+
- Maintain clean, readable code with proper documentation
|
|
20
|
+
- Use the provided data models as starting points
|
|
21
|
+
|
|
22
|
+
## Slingr Framework Patterns
|
|
23
|
+
|
|
24
|
+
### Model Definition
|
|
25
|
+
|
|
26
|
+
Always use shortcut decorators that combine @Field and type-specific decorators:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { BaseDataModel, DataModel, TextField, EmailField, IntegerField } from '@slingr/framework-backend';
|
|
30
|
+
|
|
31
|
+
@DataModel()
|
|
32
|
+
export class User extends BaseDataModel {
|
|
33
|
+
@TextField({ required: true, maxLength: 100 })
|
|
34
|
+
name: string;
|
|
35
|
+
|
|
36
|
+
@EmailField({ required: true })
|
|
37
|
+
email: string;
|
|
38
|
+
|
|
39
|
+
@IntegerField({ min: 0, max: 120 })
|
|
40
|
+
age?: number;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Relationship Fields
|
|
45
|
+
|
|
46
|
+
**CRITICAL**: Relationship decorators ALWAYS require the `type` parameter.
|
|
47
|
+
|
|
48
|
+
#### Single Reference
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { ReferenceField } from '@slingr/framework-backend';
|
|
52
|
+
|
|
53
|
+
@DataModel()
|
|
54
|
+
export class Task extends BaseDataModel {
|
|
55
|
+
@TextField({ required: true })
|
|
56
|
+
title: string;
|
|
57
|
+
|
|
58
|
+
// ALWAYS include type for references
|
|
59
|
+
@ReferenceField({ required: true, type: () => Project })
|
|
60
|
+
project: Project;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### Reference Arrays
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
@ReferenceField({ type: () => User })
|
|
68
|
+
assignees: User[];
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### Composition (Parent-Child)
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { CompositionField, OwnerReferenceField } from '@slingr/framework-backend';
|
|
75
|
+
|
|
76
|
+
@DataModel()
|
|
77
|
+
export class Order extends BaseDataModel {
|
|
78
|
+
// Composition ALWAYS requires type
|
|
79
|
+
@CompositionField({ type: () => LineItem })
|
|
80
|
+
items: LineItem[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@DataModel()
|
|
84
|
+
export class LineItem extends BaseDataModel {
|
|
85
|
+
@TextField({ required: true })
|
|
86
|
+
productName: string;
|
|
87
|
+
|
|
88
|
+
// Owner reference ALWAYS requires type
|
|
89
|
+
@OwnerReferenceField({ type: () => Order })
|
|
90
|
+
owner: Order;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### Shared Composition
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { SharedCompositionField } from '@slingr/framework-backend';
|
|
98
|
+
|
|
99
|
+
@DataModel()
|
|
100
|
+
export class Story extends BaseDataModel {
|
|
101
|
+
// Shared composition ALWAYS requires type
|
|
102
|
+
@SharedCompositionField({ type: () => Note })
|
|
103
|
+
notes: Note[];
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### All Field Types
|
|
108
|
+
|
|
109
|
+
#### String Fields
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { TextField, EmailField, HtmlField, UuidField } from '@slingr/framework-backend';
|
|
113
|
+
|
|
114
|
+
@DataModel()
|
|
115
|
+
export class Document extends BaseDataModel {
|
|
116
|
+
// Primary key - ALWAYS use UuidField
|
|
117
|
+
@UuidField({ primaryKey: true, generated: true })
|
|
118
|
+
id!: string;
|
|
119
|
+
|
|
120
|
+
// Text with validation
|
|
121
|
+
@TextField({ required: true, minLength: 3, maxLength: 100 })
|
|
122
|
+
title!: string;
|
|
123
|
+
|
|
124
|
+
// Email validation
|
|
125
|
+
@EmailField({ required: true })
|
|
126
|
+
email!: string;
|
|
127
|
+
|
|
128
|
+
// HTML content
|
|
129
|
+
@HtmlField({ maxLength: 5000 })
|
|
130
|
+
content?: string;
|
|
131
|
+
|
|
132
|
+
// Array of strings
|
|
133
|
+
@TextField()
|
|
134
|
+
tags!: string[];
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### Numeric Fields
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { IntegerField, DecimalField, MoneyField, NumberField } from '@slingr/framework-backend';
|
|
142
|
+
|
|
143
|
+
@DataModel()
|
|
144
|
+
export class Product extends BaseDataModel {
|
|
145
|
+
// Integer with constraints
|
|
146
|
+
@IntegerField({ required: true, min: 0, max: 999999 })
|
|
147
|
+
quantity!: number;
|
|
148
|
+
|
|
149
|
+
// Decimal - requires decimals and roundingType
|
|
150
|
+
@DecimalField({
|
|
151
|
+
decimals: 4,
|
|
152
|
+
roundingType: 'roundHalfToEven',
|
|
153
|
+
positive: true,
|
|
154
|
+
})
|
|
155
|
+
weight!: string;
|
|
156
|
+
|
|
157
|
+
// Money - requires decimals and roundingType
|
|
158
|
+
@MoneyField({
|
|
159
|
+
decimals: 2,
|
|
160
|
+
roundingType: 'roundHalfToEven',
|
|
161
|
+
min: '0',
|
|
162
|
+
})
|
|
163
|
+
price!: string;
|
|
164
|
+
|
|
165
|
+
// Floating point number
|
|
166
|
+
@NumberField({ min: 0, max: 1 })
|
|
167
|
+
discount?: number;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### Boolean and DateTime
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { BooleanField, DateTimeField } from '@slingr/framework-backend';
|
|
175
|
+
|
|
176
|
+
@DataModel()
|
|
177
|
+
export class Article extends BaseDataModel {
|
|
178
|
+
@BooleanField({ required: true })
|
|
179
|
+
published: boolean = false;
|
|
180
|
+
|
|
181
|
+
@DateTimeField({ required: true })
|
|
182
|
+
publishedAt!: Date;
|
|
183
|
+
|
|
184
|
+
@DateTimeField()
|
|
185
|
+
lastModified?: Date;
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### Choice/Enum Fields
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { ChoiceField } from '@slingr/framework-backend';
|
|
193
|
+
|
|
194
|
+
enum Status {
|
|
195
|
+
Draft = 'draft',
|
|
196
|
+
Published = 'published',
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@DataModel()
|
|
200
|
+
export class Post extends BaseDataModel {
|
|
201
|
+
// ALWAYS include type for enums
|
|
202
|
+
@ChoiceField({ required: true, type: () => Status })
|
|
203
|
+
status: Status = Status.Draft;
|
|
204
|
+
|
|
205
|
+
// Array of enum values
|
|
206
|
+
@ChoiceField({ type: () => Status })
|
|
207
|
+
history!: Status[];
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Model Options
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { DataModel } from '@slingr/framework-backend';
|
|
215
|
+
|
|
216
|
+
// Simple model
|
|
217
|
+
@DataModel({
|
|
218
|
+
docs: 'User model',
|
|
219
|
+
})
|
|
220
|
+
export class User extends BaseDataModel {
|
|
221
|
+
// fields...
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Model with CRUD actions
|
|
225
|
+
@DataModel({
|
|
226
|
+
docs: 'Task with auto-generated CRUD',
|
|
227
|
+
crud: {
|
|
228
|
+
generate: true,
|
|
229
|
+
api: 'gql', // Expose via GraphQL
|
|
230
|
+
actions: ['create', 'findById', 'findBy', 'update', 'deleteById'],
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
export class Task extends BaseDataModel {
|
|
234
|
+
// fields...
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Model with global validation
|
|
238
|
+
@DataModel({
|
|
239
|
+
validation: (model: PasswordChange) => {
|
|
240
|
+
const errors = [];
|
|
241
|
+
if (model.newPassword !== model.confirmPassword) {
|
|
242
|
+
errors.push({
|
|
243
|
+
constraint: 'passwordMismatch',
|
|
244
|
+
message: 'Passwords do not match',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return errors;
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
export class PasswordChange extends BaseDataModel {
|
|
251
|
+
@TextField({ required: true })
|
|
252
|
+
newPassword!: string;
|
|
253
|
+
|
|
254
|
+
@TextField({ required: true })
|
|
255
|
+
confirmPassword!: string;
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Actions Framework
|
|
260
|
+
|
|
261
|
+
#### ModelAction (Class-level actions)
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { Action, ModelAction } from '@slingr/framework-backend';
|
|
265
|
+
|
|
266
|
+
@DataModel()
|
|
267
|
+
class CreateTaskParams extends BaseDataModel {
|
|
268
|
+
@TextField({ required: true })
|
|
269
|
+
title!: string;
|
|
270
|
+
|
|
271
|
+
@ReferenceField({ required: true, type: () => Project })
|
|
272
|
+
project!: Project;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
@Action({
|
|
276
|
+
type: 'write',
|
|
277
|
+
api: 'gql',
|
|
278
|
+
model: Task,
|
|
279
|
+
params: CreateTaskParams,
|
|
280
|
+
returns: Task,
|
|
281
|
+
})
|
|
282
|
+
export class CreateTask extends ModelAction<Task, CreateTaskParams, Task> {
|
|
283
|
+
async execute(params: CreateTaskParams): Promise<Task> {
|
|
284
|
+
const task = new Task();
|
|
285
|
+
task.title = params.title;
|
|
286
|
+
task.project = params.project;
|
|
287
|
+
return task;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### ObjectAction (Instance-level actions)
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
import { ObjectAction } from '@slingr/framework-backend';
|
|
296
|
+
|
|
297
|
+
@Action({
|
|
298
|
+
type: 'write',
|
|
299
|
+
api: 'gql',
|
|
300
|
+
model: Task,
|
|
301
|
+
returns: Task,
|
|
302
|
+
})
|
|
303
|
+
export class StartTask extends ObjectAction<Task, void, Task> {
|
|
304
|
+
async execute(task: Task): Promise<Task> {
|
|
305
|
+
task.status = 'inProgress';
|
|
306
|
+
task.startedAt = new Date();
|
|
307
|
+
return task;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async canExecute(task: Task): Promise<boolean | string> {
|
|
311
|
+
if (task.status !== 'todo') {
|
|
312
|
+
return 'Task must be in todo status';
|
|
313
|
+
}
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### GlobalAction (Application-level actions)
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
import { GlobalAction } from '@slingr/framework-backend';
|
|
323
|
+
|
|
324
|
+
@DataModel()
|
|
325
|
+
class StatsResult extends BaseDataModel {
|
|
326
|
+
@IntegerField()
|
|
327
|
+
totalTasks!: number;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@Action({
|
|
331
|
+
type: 'read',
|
|
332
|
+
api: 'gql',
|
|
333
|
+
returns: StatsResult,
|
|
334
|
+
})
|
|
335
|
+
export class GetStats extends GlobalAction<void, StatsResult> {
|
|
336
|
+
async execute(): Promise<StatsResult> {
|
|
337
|
+
const stats = new StatsResult();
|
|
338
|
+
stats.totalTasks = 100;
|
|
339
|
+
return stats;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Permissions Management
|
|
345
|
+
|
|
346
|
+
Slingr uses CASL for fine-grained access control. Define permissions in your application:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
import { app } from '@slingr/framework-backend';
|
|
350
|
+
|
|
351
|
+
// Guest permissions (non-authenticated)
|
|
352
|
+
app.defineGuestPermissions(({ can, cannot }) => {
|
|
353
|
+
can('access', Article, { isPublic: true });
|
|
354
|
+
can('read', Article, ['id', 'title', 'body']);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Global permissions (all authenticated users)
|
|
358
|
+
app.defineGlobalPermissions((user, { can, cannot }) => {
|
|
359
|
+
// Users can manage their own profile
|
|
360
|
+
can('read', User, { id: user.id });
|
|
361
|
+
can('update', User, { id: user.id });
|
|
362
|
+
cannot('update', User, ['role', 'status']);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Role-based permissions
|
|
366
|
+
enum UserRole {
|
|
367
|
+
Admin = 'admin',
|
|
368
|
+
Editor = 'editor',
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
app.definePermissionsForRole(UserRole.Admin, (user, { can }) => {
|
|
372
|
+
can('manage', 'all'); // Full access
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
app.definePermissionsForRole(UserRole.Editor, (user, { can, cannot }) => {
|
|
376
|
+
// Conditional access to tasks
|
|
377
|
+
can('access', Task, {
|
|
378
|
+
project: {
|
|
379
|
+
members: { elemMatch: { id: { eq: user.id } } },
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
can('create', Task);
|
|
384
|
+
can('update', Task);
|
|
385
|
+
can('read', Task);
|
|
386
|
+
can('write', Task);
|
|
387
|
+
|
|
388
|
+
// Field-level restrictions
|
|
389
|
+
cannot('write', Task, ['status', 'priority']);
|
|
390
|
+
cannot('read', Task, ['internalNotes']);
|
|
391
|
+
|
|
392
|
+
// Action permissions
|
|
393
|
+
can('execute', CreateTask);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Callback-based permissions
|
|
397
|
+
app.definePermissionsForRole(UserRole.Editor, (user, { can }) => {
|
|
398
|
+
can('update', Task, task => {
|
|
399
|
+
return task.assignee?.id === user.id;
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Permission Actions
|
|
405
|
+
|
|
406
|
+
- `manage` - Matches any action (admin)
|
|
407
|
+
- `access` - Access to objects (CRUD)
|
|
408
|
+
- `create` - Create new objects
|
|
409
|
+
- `update` - Update existing objects
|
|
410
|
+
- `delete` - Delete objects
|
|
411
|
+
- `read` - Read specific fields
|
|
412
|
+
- `write` - Write specific fields
|
|
413
|
+
- `execute` - Execute actions
|
|
414
|
+
|
|
415
|
+
### Permission Conditions
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
// Field conditions
|
|
419
|
+
{ eq: value } // Equal
|
|
420
|
+
{ ne: value } // Not equal
|
|
421
|
+
{ gt: value } // Greater than
|
|
422
|
+
{ gte: value } // Greater than or equal
|
|
423
|
+
{ lt: value } // Less than
|
|
424
|
+
{ lte: value } // Less than or equal
|
|
425
|
+
{ in: [values] } // In array
|
|
426
|
+
{ nin: [values] } // Not in array
|
|
427
|
+
|
|
428
|
+
// Array conditions
|
|
429
|
+
{ elemMatch: query } // Element matches
|
|
430
|
+
{ all: [values] } // Has all values
|
|
431
|
+
{ size: number } // Array length
|
|
432
|
+
|
|
433
|
+
// Example
|
|
434
|
+
can('read', Article, {
|
|
435
|
+
status: { in: ['published', 'featured'] },
|
|
436
|
+
views: { gte: 1000 }
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Common Mistakes to Avoid
|
|
441
|
+
|
|
442
|
+
❌ **Missing type**:
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
@ReferenceField({ required: true })
|
|
446
|
+
project: Project; // ERROR: type is required!
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
✅ **Correct**:
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
@ReferenceField({ required: true, type: () => Project })
|
|
453
|
+
project: Project;
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
❌ **Using generic @RelationshipField**:
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
@RelationshipField({ type: 'reference', type: () => Project })
|
|
460
|
+
project: Project;
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
✅ **Use specific shortcuts**:
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
@ReferenceField({ type: () => Project })
|
|
467
|
+
project: Project;
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Reference Field Options
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
// With onDelete behavior
|
|
474
|
+
@ReferenceField({
|
|
475
|
+
type: () => Project,
|
|
476
|
+
onDelete: 'removeReference' // or 'delete' or 'nothing'
|
|
477
|
+
})
|
|
478
|
+
project: Project;
|
|
479
|
+
|
|
480
|
+
// With filter for available records
|
|
481
|
+
@ReferenceField({
|
|
482
|
+
type: () => Project,
|
|
483
|
+
filter: (task) => ({ status: 'active' })
|
|
484
|
+
})
|
|
485
|
+
activeProjects: Project[];
|
|
486
|
+
|
|
487
|
+
// Lazy vs eager loading
|
|
488
|
+
@ReferenceField({
|
|
489
|
+
type: () => Project,
|
|
490
|
+
load: false // default for references (lazy)
|
|
491
|
+
})
|
|
492
|
+
project: Project;
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Field Decorators Reference
|
|
496
|
+
|
|
497
|
+
- **@TextField** - String fields with validation (maxLength, minLength, regex)
|
|
498
|
+
- **@EmailField** - Email validation
|
|
499
|
+
- **@HtmlField** - HTML content
|
|
500
|
+
- **@UuidField** - UUID fields (use for primary keys with generated: true)
|
|
501
|
+
- **@IntegerField** - Integer numbers
|
|
502
|
+
- **@DecimalField** - Precise decimals (requires decimals and roundingType)
|
|
503
|
+
- **@MoneyField** - Money amounts (requires decimals and roundingType)
|
|
504
|
+
- **@NumberField** - Floating point numbers
|
|
505
|
+
- **@BooleanField** - Boolean values
|
|
506
|
+
- **@DateTimeField** - Date and time
|
|
507
|
+
- **@ChoiceField** - Enum selections (requires type for enums)
|
|
508
|
+
- **@ReferenceField** - Model references (requires type)
|
|
509
|
+
- **@CompositionField** - Parent-child relationships (requires type)
|
|
510
|
+
- **@SharedCompositionField** - Shared compositions (requires type)
|
|
511
|
+
- **@OwnerReferenceField** - Child to parent reference (requires type)
|
|
512
|
+
|
|
513
|
+
## Build & Compilation
|
|
514
|
+
|
|
515
|
+
### ALWAYS use `slingr build` — never `npm run build` directly
|
|
516
|
+
|
|
517
|
+
Slingr apps have **auto-generated files** in `backend/generated/` and `frontend/generated/` (view registries, GraphQL schema, SDK). These files are produced by `slingr sync-metadata` and must exist before TypeScript can compile. Running `npm run build` directly will fail with errors like:
|
|
518
|
+
|
|
519
|
+
```
|
|
520
|
+
generated/viewsRegistry.ts:25:19 - error TS2307: Cannot find module '../src/views/...'
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
**Always use:**
|
|
524
|
+
|
|
525
|
+
```bash
|
|
526
|
+
slingr build # sync metadata + compile backend + build frontend
|
|
527
|
+
slingr build --skip-frontend # sync metadata + compile backend only
|
|
528
|
+
slingr build --skip-metadata # skip metadata (if already up-to-date)
|
|
529
|
+
slingr build --verbose # show detailed output
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### Never edit files in `generated/`
|
|
533
|
+
|
|
534
|
+
Files in `backend/generated/` and `frontend/generated/` are **auto-generated**. Never edit them manually — changes will be overwritten on the next `slingr sync-metadata` or `slingr build`.
|
|
535
|
+
|
|
536
|
+
If you see TypeScript errors in `generated/` files:
|
|
537
|
+
1. Run `slingr build` to regenerate them
|
|
538
|
+
2. Do **not** try to fix the errors by editing `generated/` files
|
|
539
|
+
|
|
540
|
+
### `slingr sync-metadata` (metadata only)
|
|
541
|
+
|
|
542
|
+
If you only need to regenerate the metadata without recompiling:
|
|
543
|
+
|
|
544
|
+
```bash
|
|
545
|
+
slingr sync-metadata # regenerate all generated files
|
|
546
|
+
slingr sync-metadata --skip-views # skip view registries
|
|
547
|
+
slingr sync-metadata --skip-schema # skip GraphQL schema
|
|
548
|
+
slingr sync-metadata --skip-sdk # skip GraphQL SDK
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## Class API Reference — Overridable Methods and Fields
|
|
554
|
+
|
|
555
|
+
When generating code that extends framework base classes, only use the methods and fields listed below. Do NOT suggest private or internal members.
|
|
556
|
+
|
|
557
|
+
### BaseDataModel (backend)
|
|
558
|
+
|
|
559
|
+
Overridable public methods:
|
|
560
|
+
|
|
561
|
+
- `onRefresh?(changedFields: string[]): void | Promise<void>` — React to field changes server-side.
|
|
562
|
+
- `validate(): Promise<ValidationError[]>` — Run validation rules.
|
|
563
|
+
- `toJSON(): Record<string, any>` — Serialize to plain object.
|
|
564
|
+
- `calculate(maxIterations?: number): Promise<void>` — Recalculate manual calculated fields.
|
|
565
|
+
- `filter(): void` — Clear unavailable field values.
|
|
566
|
+
|
|
567
|
+
Static methods:
|
|
568
|
+
|
|
569
|
+
- `fromJSON(json): T` — Deserialize a plain object into a typed model instance.
|
|
570
|
+
|
|
571
|
+
### Actions (backend)
|
|
572
|
+
|
|
573
|
+
#### GlobalAction\<P, R\>
|
|
574
|
+
|
|
575
|
+
- `async execute(params: P): Promise<R>` — (required) Implement the action logic.
|
|
576
|
+
- `async canExecute(): Promise<boolean | string>` — Return `false` or error string to prevent execution.
|
|
577
|
+
|
|
578
|
+
#### ModelAction\<M, P, R\>
|
|
579
|
+
|
|
580
|
+
- `async execute(params: P): Promise<R>` — (required) Implement the action logic.
|
|
581
|
+
- `async canExecute(): Promise<boolean | string>` — Return `false` or error string to prevent execution.
|
|
582
|
+
|
|
583
|
+
#### ObjectAction\<M, P, R\>
|
|
584
|
+
|
|
585
|
+
- `async execute(target: M, params: P): Promise<R>` — (required) Implement the action logic.
|
|
586
|
+
- `async canExecute(target: M): Promise<boolean | string>` — Return `false` or error string to prevent execution.
|
|
587
|
+
|
|
588
|
+
### Workflow Actions (backend)
|
|
589
|
+
|
|
590
|
+
All workflow actions share the action pattern plus:
|
|
591
|
+
|
|
592
|
+
- `callStep(stepName: string, ...args): Promise<any>` — Invoke a `@Step()` method.
|
|
593
|
+
- `reportProgress(progress: number): void` — Report execution progress (0–100).
|
|
594
|
+
|
|
595
|
+
### BaseQueue (backend)
|
|
596
|
+
|
|
597
|
+
Overridable lifecycle hooks:
|
|
598
|
+
|
|
599
|
+
- `onWorkflowActive(workflow: Workflow): void | Promise<void>`
|
|
600
|
+
- `onWorkflowCompleted(workflow: Workflow, result: any): void | Promise<void>`
|
|
601
|
+
- `onWorkflowFailed(workflow: Workflow, error: Error): void | Promise<void>`
|
|
602
|
+
- `onWorkflowProgress(workflow: Workflow, progress: number): void | Promise<void>`
|
|
603
|
+
|
|
604
|
+
Public API:
|
|
605
|
+
|
|
606
|
+
- `getActiveCount()`, `getCompletedCount()`, `getFailedCount()`, `getName()`, `getConfig()`
|
|
607
|
+
|
|
608
|
+
### BaseLayout (frontend)
|
|
609
|
+
|
|
610
|
+
Overridable configuration:
|
|
611
|
+
|
|
612
|
+
- `navigation` — `'mix'` | `'left'` | `'top'`
|
|
613
|
+
- `contentWidth` — `'fluid'` | `'fixed'`
|
|
614
|
+
- `features` — Controls header, footer, mainMenu, secondaryMenu, userMenu visibility.
|
|
615
|
+
- `header?`, `footer?`, `mainMenu?`, `secondaryMenu?`, `userMenu?`
|
|
616
|
+
|
|
617
|
+
Lifecycle hooks: `onMenuClick?()`, `onMenuCollapse?(collapsed: boolean)`, `onPageSwitch?()`
|
|
618
|
+
|
|
619
|
+
### CustomViewComponent (frontend)
|
|
620
|
+
|
|
621
|
+
Configuration: `header?`, `footer?`, `modal?`, `menu?`, `layout?`, `modalSize?`, `modalPosition?`
|
|
622
|
+
|
|
623
|
+
Lifecycle hooks:
|
|
624
|
+
|
|
625
|
+
- `onLoad()` — Initialize data after mount.
|
|
626
|
+
- `onLeave()` — Clean up before unmount.
|
|
627
|
+
- `onRender(): React.ReactNode` — (required) Render the view content.
|
|
628
|
+
- `onParamsChange?(prevParams, newParams)` — Handle route param changes.
|
|
629
|
+
|
|
630
|
+
Methods: `getParams()`, `getQuery()`, `openView()`, `closeView()`, `getContext()`, `isInModal()`, `setState()`, `forceUpdate()`, `app` (antd message/modal/notification)
|
|
631
|
+
|
|
632
|
+
### TableViewComponent (frontend)
|
|
633
|
+
|
|
634
|
+
Required: `tableOptions: TableViewTableOptions<T>` — (required) columns, pagination, selection. Optional: `header?`, `menu?`, `layout?`, `hideHeader?`, `persistence?`, `modalSize?`, `modalPosition?` Hook: `afterActionExecution?(response)` — Post-action hook. Static toolbar: `toolbar.modelActionButton(name)`, `toolbar.globalActionButton(name)`, etc.
|
|
635
|
+
|
|
636
|
+
### Form Views (frontend)
|
|
637
|
+
|
|
638
|
+
#### CreateViewComponent\<T\>
|
|
639
|
+
|
|
640
|
+
Config: `fields?`, `formLayout?`, `layout?`, `refreshMode?`, `refreshTriggers?`, `formProps?` Hooks: `beforeCreate()`, `afterCreated(response)`, `onRefresh(changedFields, data, prevData)`, `onRenderForm(context)`
|
|
641
|
+
|
|
642
|
+
#### EditViewComponent\<T\>
|
|
643
|
+
|
|
644
|
+
Same as Create plus: `modalSize?`, `modalPosition?` Hooks: `beforeSave()`, `afterSaved(response)`, `onSave()`, `onRefresh()`, `onRender()`, `onRenderForm()` Methods: `getFormValue(field)`, `getFormValues()`, `setFormValue(field, value)`, `setFormValues(values)`
|
|
645
|
+
|
|
646
|
+
#### ReadViewComponent\<T\>
|
|
647
|
+
|
|
648
|
+
Config: `fields?`, `formLayout?`, `layout?`, `formProps?`, `deleteFallbackPath?` Hooks: `afterActionExecution(response)`, `onRenderForm(context)` Fields: `id`, `object`, `objectActions`, `modelActions`, `globalActions` Static toolbar: `toolbar.refreshButton()`, `toolbar.editButton()`, `toolbar.deleteButton()`, `toolbar.closeButton()`, `toolbar.actionsDropdown()`
|
|
649
|
+
|
|
650
|
+
#### ActionViewComponent\<T\>
|
|
651
|
+
|
|
652
|
+
Config: `fields?`, `formLayout?`, `layout?`, `refreshMode?`, `refreshTriggers?`, `modalSize?`, `modalPosition?` Hooks: `onLoad()`, `beforeExecute()`, `onExecute()`, `afterExecuted(response)`, `onCancel()`, `onRefresh()`, `onRender()`, `onRenderForm()` Fields: `actionInfo`, `initialData`, `targetObject`, `idForRefresh`
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
# Build context: this app's root directory (the folder containing backend/ and frontend/).
|
|
3
|
+
# Usage: docker build -f backend/Dockerfile -t <image> .
|
|
4
|
+
|
|
5
|
+
# ── Stage 1: build ──────────────────────────────────────────────────────────
|
|
6
|
+
FROM node:20-alpine AS builder
|
|
7
|
+
WORKDIR /app
|
|
8
|
+
|
|
9
|
+
# Install dependencies separately to leverage Docker layer caching.
|
|
10
|
+
# Re-runs only when package.json / package-lock.json change.
|
|
11
|
+
COPY backend/package*.json ./
|
|
12
|
+
RUN npm install
|
|
13
|
+
|
|
14
|
+
# Compile TypeScript.
|
|
15
|
+
COPY backend/ ./
|
|
16
|
+
RUN npm run build
|
|
17
|
+
|
|
18
|
+
# ── Stage 2: runtime ─────────────────────────────────────────────────────────
|
|
19
|
+
FROM node:20-alpine AS runtime
|
|
20
|
+
WORKDIR /app
|
|
21
|
+
|
|
22
|
+
COPY --from=builder /app/dist/ dist/
|
|
23
|
+
COPY --from=builder /app/node_modules/ node_modules/
|
|
24
|
+
COPY --from=builder /app/package.json ./
|
|
25
|
+
|
|
26
|
+
# Cloud Run injects $PORT; default to 3000 for local use.
|
|
27
|
+
ENV PORT=3000
|
|
28
|
+
EXPOSE 3000
|
|
29
|
+
|
|
30
|
+
CMD ["node", "dist/src/App.js"]
|