@apso/cli 0.8.6 → 0.10.1
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/README.md +221 -1653
- package/dist/commands/config.d.ts +13 -0
- package/dist/commands/config.js +114 -0
- package/dist/commands/deploy.d.ts +11 -0
- package/dist/commands/deploy.js +141 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.js +95 -0
- package/dist/commands/generate.d.ts +12 -0
- package/dist/commands/generate.js +179 -0
- package/dist/commands/init.d.ts +16 -0
- package/dist/commands/init.js +313 -0
- package/dist/commands/link.d.ts +11 -0
- package/dist/commands/link.js +139 -0
- package/dist/commands/login.d.ts +22 -0
- package/dist/commands/login.js +345 -0
- package/dist/commands/logout.d.ts +9 -0
- package/dist/commands/logout.js +38 -0
- package/dist/commands/logs.d.ts +10 -0
- package/dist/commands/logs.js +64 -0
- package/dist/commands/mcp/serve.d.ts +11 -0
- package/dist/commands/mcp/serve.js +818 -0
- package/dist/commands/migrate.d.ts +11 -0
- package/dist/commands/migrate.js +137 -0
- package/dist/commands/open.d.ts +10 -0
- package/dist/commands/open.js +79 -0
- package/dist/commands/projects.d.ts +10 -0
- package/dist/commands/projects.js +106 -0
- package/dist/commands/schema/diff.d.ts +7 -0
- package/dist/commands/schema/diff.js +63 -0
- package/dist/commands/schema/pull.d.ts +9 -0
- package/dist/commands/schema/pull.js +81 -0
- package/dist/commands/schema/push.d.ts +9 -0
- package/dist/commands/schema/push.js +97 -0
- package/dist/commands/schema/validate.d.ts +9 -0
- package/dist/commands/schema/validate.js +90 -0
- package/dist/commands/server/new.d.ts +2 -19
- package/dist/commands/server/new.js +6 -192
- package/dist/commands/server/scaffold.d.ts +2 -25
- package/dist/commands/server/scaffold.js +6 -312
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.js +53 -0
- package/dist/commands/unlink.d.ts +9 -0
- package/dist/commands/unlink.js +47 -0
- package/dist/commands/whoami.d.ts +10 -0
- package/dist/commands/whoami.js +97 -0
- package/dist/lib/api/client.js +1 -1
- package/dist/lib/api/services.js +3 -3
- package/dist/lib/api/types.d.ts +9 -2
- package/dist/lib/apsorc-parser.d.ts +0 -19
- package/dist/lib/apsorc-parser.js +2 -73
- package/dist/lib/guards.d.ts +3 -20
- package/dist/lib/guards.js +1 -113
- package/dist/lib/index.d.ts +1 -9
- package/dist/lib/index.js +1 -18
- package/dist/lib/migrate/entity-generator.d.ts +14 -0
- package/dist/lib/migrate/entity-generator.js +7 -0
- package/dist/lib/migrate/sandbox.d.ts +42 -0
- package/dist/lib/migrate/sandbox.js +337 -0
- package/dist/lib/migrate/snapshot.d.ts +80 -0
- package/dist/lib/migrate/snapshot.js +100 -0
- package/dist/lib/templates/python/models/model-col-datetime.eta +1 -1
- package/dist/lib/templates/python/models/model-col-string.eta +1 -1
- package/dist/lib/templates/python/models/model-col-text.eta +1 -1
- package/dist/lib/templates/python/models/model-col-varchar.eta +1 -1
- package/dist/lib/utils/field.d.ts +12 -0
- package/dist/lib/utils/field.js +80 -1
- package/dist/lib/utils/schema-convert.d.ts +71 -0
- package/dist/lib/utils/schema-convert.js +169 -0
- package/dist/lib/utils/spinner.d.ts +19 -0
- package/dist/lib/utils/spinner.js +87 -0
- package/dist/lib/utils/template.d.ts +12 -0
- package/dist/lib/utils/template.js +49 -0
- package/npm-shrinkwrap.json +18118 -0
- package/oclif.manifest.json +547 -41
- package/package.json +26 -5
- package/dist/lib/controller.d.ts +0 -10
- package/dist/lib/controller.js +0 -99
- package/dist/lib/dto.d.ts +0 -16
- package/dist/lib/dto.js +0 -92
- package/dist/lib/entity.d.ts +0 -19
- package/dist/lib/entity.js +0 -56
- package/dist/lib/enums.d.ts +0 -6
- package/dist/lib/enums.js +0 -28
- package/dist/lib/gql-dto.d.ts +0 -3
- package/dist/lib/gql-dto.js +0 -39
- package/dist/lib/index-module.d.ts +0 -13
- package/dist/lib/index-module.js +0 -26
- package/dist/lib/module.d.ts +0 -13
- package/dist/lib/module.js +0 -42
- package/dist/lib/service.d.ts +0 -2
- package/dist/lib/service.js +0 -61
package/README.md
CHANGED
|
@@ -1,1786 +1,354 @@
|
|
|
1
1
|
# Apso CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Define a schema. Get a production API. Keep the code.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Apso generates production-ready backend services from a JSON schema file. You get real framework code (NestJS, FastAPI, or Gin) that you own, run anywhere, and extend with standard patterns. For the complete documentation, see [Apso Docs](https://docs.apso.ai).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
# 1. Install CLI
|
|
9
|
-
npm install -g @apso/apso-cli
|
|
10
|
-
|
|
11
|
-
# 2. Create new project
|
|
12
|
-
apso server new --name myapp
|
|
13
|
-
|
|
14
|
-
# 3. Edit .apsorc to define your schema (see examples below)
|
|
15
|
-
|
|
16
|
-
# 4. Generate code
|
|
17
|
-
apso server scaffold
|
|
18
|
-
|
|
19
|
-
# 5. Start database & provision schema
|
|
20
|
-
npm run compose && npm run provision
|
|
21
|
-
|
|
22
|
-
# 6. Run development server
|
|
23
|
-
npm run start:dev
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Table of Contents
|
|
29
|
-
|
|
30
|
-
- [Quick Start](#quick-start)
|
|
31
|
-
- [Usage](#usage)
|
|
32
|
-
- [Important: Never Modify Autogen Files](#-important-never-add-custom-code-to-autogen-files)
|
|
33
|
-
- [Common First-Time Mistakes](#️-common-first-time-mistakes)
|
|
34
|
-
- [Local Development](#local-development)
|
|
35
|
-
- [Populating an .apsorc File](#populating-an-apsorc-file)
|
|
36
|
-
- [Auto-Generated Code Reference](#auto-generated-code-reference)
|
|
37
|
-
- [Relationships](#relationships)
|
|
38
|
-
- [Authentication (Bring Your Own Auth)](#authentication-bring-your-own-auth)
|
|
39
|
-
- [Data Scoping (Multi-Tenant Isolation)](#data-scoping-multi-tenant-isolation)
|
|
40
|
-
- [Authentication + Scoping: Working Together](#authentication--scoping-working-together)
|
|
41
|
-
- [Schema Reference](#schema-reference)
|
|
42
|
-
- [Debugging](#debugging)
|
|
43
|
-
- [Commands](#commands)
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
# Usage
|
|
48
|
-
|
|
49
|
-
Follow these steps to create and run a new APSO server project:
|
|
50
|
-
|
|
51
|
-
1. **Install the CLI globally:**
|
|
52
|
-
```sh
|
|
53
|
-
npm install -g @apso/apso-cli
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
2. **Initialize a new server project:**
|
|
57
|
-
```sh
|
|
58
|
-
apso server new --name <PROJECT_NAME>
|
|
59
|
-
```
|
|
60
|
-
This creates a new project folder with the necessary boilerplate.
|
|
61
|
-
|
|
62
|
-
3. **Define your database schema:**
|
|
63
|
-
Edit the `.apsorc` file in your new project folder to describe your entities and relationships. See the [Populating an .apsorc File](#populating-an-apsorc-file) section for details and examples.
|
|
64
|
-
|
|
65
|
-
4. **Generate code and database entities:**
|
|
66
|
-
```sh
|
|
67
|
-
apso server scaffold
|
|
68
|
-
```
|
|
69
|
-
This command generates all relevant modules and entity code based on your `.apsorc` file.
|
|
70
|
-
|
|
71
|
-
5. **Configure your database connection:**
|
|
72
|
-
Update your project's `.env` file with the database credentials you want to use.
|
|
73
|
-
|
|
74
|
-
6. **Start the local Postgres instance (Docker):**
|
|
75
|
-
```sh
|
|
76
|
-
npm run compose
|
|
77
|
-
```
|
|
78
|
-
This command uses Docker Compose to start a local Postgres database instance.
|
|
79
|
-
|
|
80
|
-
7. **Provision your schema/database:**
|
|
81
|
-
```sh
|
|
82
|
-
npm run provision
|
|
83
|
-
```
|
|
84
|
-
This sets up your new schema instance in the database.
|
|
85
|
-
|
|
86
|
-
8. **(Optional) Enable automatic model sync for local development:**
|
|
87
|
-
If you want to skip manual migrations and always sync your models with the database (useful for rapid prototyping or starting from scratch), set the following in your `.env` file:
|
|
88
|
-
```env
|
|
89
|
-
DATABASE_SYNC=true
|
|
90
|
-
```
|
|
91
|
-
With this setting, your models will be automatically synced to the database on startup.
|
|
92
|
-
|
|
93
|
-
> For more details on configuring your schema, see the [Populating an .apsorc File](#populating-an-apsorc-file) section below.
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
## 📢 Important: Never Add Custom Code to Autogen Files
|
|
98
|
-
|
|
99
|
-
Apso CLI generates all files in the `autogen/` directory automatically.
|
|
100
|
-
**Any changes you make directly to these files will be overwritten the next time you run Apso CLI.**
|
|
101
|
-
To keep your custom logic safe and maintainable, always use the `extensions/` directory for any customizations.
|
|
102
|
-
|
|
103
|
-
### How to Extend Apso-Generated Entities
|
|
104
|
-
|
|
105
|
-
#### 1. **Never modify files in `autogen/`**
|
|
106
|
-
|
|
107
|
-
- All files in `src/autogen/` are managed by Apso CLI.
|
|
108
|
-
- These include entities, services, controllers, DTOs, and modules.
|
|
109
|
-
- **Do not add custom endpoints, business logic, or integrations here.**
|
|
110
|
-
|
|
111
|
-
#### 2. **Add custom logic in `extensions/`**
|
|
112
|
-
|
|
113
|
-
- For each entity you want to extend, create a corresponding folder in `src/extensions/[[EntityName]]/`.
|
|
114
|
-
- Add your custom service, controller, and DTOs here.
|
|
115
|
-
- You can inject and extend the autogen service in your extension service.
|
|
116
|
-
|
|
117
|
-
#### 3. **Example Directory Structure**
|
|
118
|
-
|
|
119
|
-
```
|
|
120
|
-
src/
|
|
121
|
-
autogen/
|
|
122
|
-
LambdaDeployment/
|
|
123
|
-
LambdaDeployment.service.ts # DO NOT MODIFY
|
|
124
|
-
LambdaDeployment.controller.ts
|
|
125
|
-
...
|
|
126
|
-
extensions/
|
|
127
|
-
LambdaDeployment/
|
|
128
|
-
LambdaDeployment.service.ts # Add custom logic here
|
|
129
|
-
LambdaDeployment.controller.ts
|
|
130
|
-
...
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
#### 4. **Example: Extending LambdaDeployment**
|
|
7
|
+
## Install
|
|
134
8
|
|
|
135
|
-
**
|
|
136
|
-
```typescript
|
|
137
|
-
// src/extensions/LambdaDeployment/LambdaDeployment.service.ts
|
|
138
|
-
import { Injectable } from '@nestjs/common';
|
|
139
|
-
import { LambdaDeploymentService as AutogenLambdaDeploymentService } from '../../autogen/LambdaDeployment/LambdaDeployment.service';
|
|
9
|
+
**Homebrew** (macOS / Linux)
|
|
140
10
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
async deployWithLocalService(...) { ... }
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
**Custom Controller:**
|
|
149
|
-
```typescript
|
|
150
|
-
// src/extensions/LambdaDeployment/LambdaDeployment.controller.ts
|
|
151
|
-
import { Controller } from '@nestjs/common';
|
|
152
|
-
import { LambdaDeploymentService } from './LambdaDeployment.service';
|
|
153
|
-
|
|
154
|
-
@Controller('lambda-deployment')
|
|
155
|
-
export class LambdaDeploymentController {
|
|
156
|
-
constructor(private readonly lambdaDeploymentService: LambdaDeploymentService) {}
|
|
157
|
-
|
|
158
|
-
// Add custom endpoints here
|
|
159
|
-
}
|
|
11
|
+
```bash
|
|
12
|
+
brew tap apsoai/tap
|
|
13
|
+
brew install apso
|
|
160
14
|
```
|
|
161
15
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
- Keeps your custom business logic safe from being overwritten.
|
|
165
|
-
- Makes it easy to regenerate your API as your data model evolves.
|
|
166
|
-
- Maintains a clean separation between generated code and your application logic.
|
|
167
|
-
|
|
168
|
-
#### 6. **Best Practices**
|
|
169
|
-
|
|
170
|
-
- Only use the autogen files for base CRUD and entity logic.
|
|
171
|
-
- Place all custom endpoints, integrations, and business logic in the `extensions/` directory.
|
|
172
|
-
- If you need to override or extend a method, subclass the autogen service in your extension service.
|
|
173
|
-
|
|
174
|
-
---
|
|
16
|
+
**npm**
|
|
175
17
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
> Never modify files in `src/autogen/`.
|
|
179
|
-
> This ensures your work is safe and your project remains maintainable as you evolve your data model with Apso CLI.
|
|
180
|
-
|
|
181
|
-
---
|
|
182
|
-
|
|
183
|
-
## ⚠️ Common First-Time Mistakes
|
|
184
|
-
|
|
185
|
-
### 1. Modifying `autogen/` Files
|
|
186
|
-
**Problem:** Changes get overwritten on next scaffold
|
|
187
|
-
**Solution:** Always use `extensions/` directory for custom code
|
|
188
|
-
|
|
189
|
-
### 2. Defining Both Sides of Relationships
|
|
190
|
-
**Problem:** Duplicate properties and TypeScript errors
|
|
191
|
-
**Solution:** Define relationships **once** - Apso auto-generates the inverse side
|
|
192
|
-
|
|
193
|
-
Example:
|
|
194
|
-
```json
|
|
195
|
-
// ❌ WRONG - Creates conflicts
|
|
196
|
-
{ "from": "User", "to": "Workspace", "type": "OneToMany" }
|
|
197
|
-
{ "from": "Workspace", "to": "User", "type": "ManyToOne" }
|
|
198
|
-
|
|
199
|
-
// ✅ CORRECT - Define one side only
|
|
200
|
-
{ "from": "Workspace", "to": "User", "type": "ManyToOne", "to_name": "owner" }
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @apso/cli
|
|
201
20
|
```
|
|
202
21
|
|
|
203
|
-
|
|
204
|
-
**Problem:** Server fails to start with missing table errors
|
|
205
|
-
**Solution:** Always run `npm run provision` after scaffold
|
|
206
|
-
|
|
207
|
-
### 4. Wrong .apsorc Version
|
|
208
|
-
**Problem:** Schema doesn't generate correctly
|
|
209
|
-
**Solution:** Ensure `"version": 2` at top of .apsorc
|
|
210
|
-
|
|
211
|
-
### 5. Forgetting to Start Docker
|
|
212
|
-
**Problem:** Database connection refused errors
|
|
213
|
-
**Solution:** Run `npm run compose` before starting server
|
|
22
|
+
Requires Node.js 18.0 or higher.
|
|
214
23
|
|
|
215
|
-
|
|
24
|
+
## Connect
|
|
216
25
|
|
|
217
|
-
|
|
26
|
+
Authenticate with the Apso platform:
|
|
218
27
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
```sh-session
|
|
222
|
-
npm install
|
|
223
|
-
npm run build && npm link
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
This will build the apso cli and make it available for use globally on your machine. Now make a new directory where you want to setup a new backend service. Run the below command in order to create a new service boilerplate. This will clone the apso-service-template from github.
|
|
227
|
-
|
|
228
|
-
Incase you face permission denied issue make sure that you have SSH key added in github and your local machine. Follow this [link](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent) for SSH keys generation.
|
|
229
|
-
|
|
230
|
-
```sh-session
|
|
231
|
-
apso server new -n <project-name>
|
|
28
|
+
```bash
|
|
29
|
+
apso login
|
|
232
30
|
```
|
|
233
31
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
Sample of apsorc files for both v1 and v2 are given in apso-cli code at "test/apsorc-json" so you can check it out in order to make sure that your apsorc file follows the right pattern.
|
|
237
|
-
|
|
238
|
-
For detailed examples of configuring complex relationships, especially ManyToMany and self-referencing patterns, see the [Relationship Configuration Use Cases](./UseCases.md).
|
|
32
|
+
This opens a browser window for OAuth authentication. For CI/CD environments, use a token:
|
|
239
33
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
Install the npm modules before continuing further.
|
|
243
|
-
|
|
244
|
-
Now we will run the scaffold command which will generate the all the relevant modules for us.
|
|
245
|
-
|
|
246
|
-
```sh-session
|
|
247
|
-
apso server scaffold
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
# Populating an .apsorc File
|
|
251
|
-
|
|
252
|
-
The `.apsorc` file defines your domain model, including entities and their relationships, for APSO code generation. It is required for scaffolding your backend service.
|
|
253
|
-
|
|
254
|
-
## How to Create and Populate `.apsorc`
|
|
255
|
-
|
|
256
|
-
1. **Location**: Place the `.apsorc` file in the root of your service directory.
|
|
257
|
-
2. **Version**: Set the `version` property to `2` for the latest schema.
|
|
258
|
-
3. **Entities**: Define each domain entity, its fields, and any unique constraints.
|
|
259
|
-
4. **Relationships**: Specify how entities relate (e.g., OneToMany, ManyToOne, etc.).
|
|
260
|
-
5. **rootFolder**: Set the folder where generated code will be placed (e.g., `src`).
|
|
261
|
-
|
|
262
|
-
> **Tip:** You can find sample `.apsorc` files in `apso-cli/test/apsorc-json/` for both v1 and v2 formats.
|
|
263
|
-
|
|
264
|
-
### Example `.apsorc` v2 File
|
|
265
|
-
|
|
266
|
-
```json
|
|
267
|
-
{
|
|
268
|
-
"version": 2,
|
|
269
|
-
"rootFolder": "src",
|
|
270
|
-
"relationships": [
|
|
271
|
-
{ "from": "User", "to": "WorkspaceUser", "type": "OneToMany", "nullable": true },
|
|
272
|
-
{ "from": "Workspace", "to": "WorkspaceUser", "type": "OneToMany" },
|
|
273
|
-
{ "from": "Workspace", "to": "Application", "type": "OneToMany", "index": true },
|
|
274
|
-
{ "from": "Application", "to": "ApplicationService", "type": "OneToMany" },
|
|
275
|
-
{ "from": "Application", "to": "User", "type": "ManyToOne", "to_name": "owner" },
|
|
276
|
-
{ "from": "ApplicationService", "to": "ApplicationServiceApiKey", "type": "OneToMany" },
|
|
277
|
-
{ "from": "ApplicationService", "to": "ApplicationServiceMetric", "type": "OneToMany" },
|
|
278
|
-
{ "from": "ApplicationService", "to": "InfrastructureStack", "type": "ManyToOne", "to_name": "networkStack", "nullable": true },
|
|
279
|
-
{ "from": "ApplicationService", "to": "InfrastructureStack", "type": "ManyToOne", "to_name": "databaseStack", "nullable": true },
|
|
280
|
-
{ "from": "InfrastructureStack", "to": "InfrastructureStack", "type": "ManyToOne", "to_name": "networkStack", "nullable": true }
|
|
281
|
-
],
|
|
282
|
-
"entities": [
|
|
283
|
-
{
|
|
284
|
-
"name": "User",
|
|
285
|
-
"created_at": true,
|
|
286
|
-
"updated_at": true,
|
|
287
|
-
"fields": [
|
|
288
|
-
{ "name": "cognito_id", "type": "text", "unique": true },
|
|
289
|
-
{ "name": "email", "type": "text", "length": 255, "is_email": true },
|
|
290
|
-
{ "name": "fullName", "type": "text", "nullable": true }
|
|
291
|
-
]
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
"name": "Workspace",
|
|
295
|
-
"created_at": true,
|
|
296
|
-
"updated_at": true,
|
|
297
|
-
"fields": [
|
|
298
|
-
{ "name": "name", "type": "text" }
|
|
299
|
-
]
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
"name": "WorkspaceUser",
|
|
303
|
-
"created_at": true,
|
|
304
|
-
"updated_at": true,
|
|
305
|
-
"fields": [
|
|
306
|
-
{ "name": "email", "type": "text", "length": 255, "is_email": true },
|
|
307
|
-
{ "name": "invite_code", "type": "text", "length": 64 },
|
|
308
|
-
{ "name": "role", "type": "enum", "values": ["User", "Admin"], "default": "Admin" },
|
|
309
|
-
{ "name": "status", "type": "enum", "values": ["Active", "Invited", "Inactive", "Deleted"] },
|
|
310
|
-
{ "name": "activeAt", "type": "date", "nullable": true }
|
|
311
|
-
]
|
|
312
|
-
},
|
|
313
|
-
{
|
|
314
|
-
"name": "Application",
|
|
315
|
-
"created_at": true,
|
|
316
|
-
"updated_at": true,
|
|
317
|
-
"fields": [
|
|
318
|
-
{ "name": "name", "type": "text" },
|
|
319
|
-
{ "name": "status", "type": "enum", "values": ["Active", "Deleted"] }
|
|
320
|
-
]
|
|
321
|
-
}
|
|
322
|
-
// ... more entities as needed ...
|
|
323
|
-
]
|
|
324
|
-
}
|
|
34
|
+
```bash
|
|
35
|
+
apso login --token <api-token>
|
|
325
36
|
```
|
|
326
37
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
## Auto-Generated Code Reference
|
|
330
|
-
|
|
331
|
-
This section details the code that Apso CLI automatically generates from your `.apsorc` configuration, including fields, TypeORM mappings, validation, and naming conventions. Understanding these rules will help you predict and control your generated backend code.
|
|
332
|
-
|
|
333
|
-
### 1. Auto-Generated Fields
|
|
334
|
-
|
|
335
|
-
#### Primary Keys
|
|
336
|
-
- Every entity automatically receives an `id` field, even if not defined in the `fields` array.
|
|
337
|
-
- Decorated as `@PrimaryGeneratedColumn()` (auto-incrementing integer).
|
|
338
|
-
- Type: `number`.
|
|
339
|
-
|
|
340
|
-
```typescript
|
|
341
|
-
@PrimaryGeneratedColumn()
|
|
342
|
-
id: number;
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
#### Timestamps
|
|
346
|
-
- If `"created_at": true` is set on the entity, generates:
|
|
347
|
-
```typescript
|
|
348
|
-
@CreateDateColumn()
|
|
349
|
-
created_at: Date;
|
|
350
|
-
```
|
|
351
|
-
- If `"updated_at": true` is set on the entity, generates:
|
|
352
|
-
```typescript
|
|
353
|
-
@UpdateDateColumn()
|
|
354
|
-
updated_at: Date;
|
|
355
|
-
```
|
|
356
|
-
- These are in addition to any fields you define.
|
|
357
|
-
|
|
358
|
-
#### Foreign Key Fields
|
|
359
|
-
- For each relationship, Apso generates the foreign key column and TypeORM decorators.
|
|
360
|
-
- Example:
|
|
361
|
-
```json
|
|
362
|
-
{ "from": "Workspace", "to": "User", "type": "ManyToOne", "to_name": "owner" }
|
|
363
|
-
```
|
|
364
|
-
Generates:
|
|
365
|
-
```typescript
|
|
366
|
-
@ManyToOne(() => User)
|
|
367
|
-
@JoinColumn({ name: 'ownerId' })
|
|
368
|
-
owner: User;
|
|
369
|
-
|
|
370
|
-
@Column({ type: 'integer' })
|
|
371
|
-
ownerId: number;
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
### 2. Field Type Mapping Table
|
|
375
|
-
|
|
376
|
-
> **⚠️ PostGIS Requirements:** The following spatial data types require the PostGIS extension to be installed in your PostgreSQL database:
|
|
377
|
-
> ```sql
|
|
378
|
-
> CREATE EXTENSION IF NOT EXISTS postgis;
|
|
379
|
-
> ```
|
|
380
|
-
> Ensure PostGIS is installed before using any of the spatial data types listed below.
|
|
381
|
-
|
|
382
|
-
| Apso Field Type | TypeORM Column Decorator | Auto-Applied Validation | Notes |
|
|
383
|
-
|-----------------|-----------------------------------------|----------------------------------------|---------------------------------------|
|
|
384
|
-
| `text` | `@Column({ type: 'text' })` | `@IsString()`, `@IsNotEmpty()` | With `length`: `@MaxLength(n)` |
|
|
385
|
-
| `json` | `@Column('jsonb')` | None | Uses JSONB in PostgreSQL |
|
|
386
|
-
| `enum` | `@Column({ type: 'enum', enum: [...] })`| None | Values array becomes enum |
|
|
387
|
-
| `boolean` | `@Column({ type: 'boolean' })` | `@IsBoolean()` | Default values supported |
|
|
388
|
-
| `integer` | `@Column({ type: 'integer' })` | `@IsNumber()` | |
|
|
389
|
-
| `decimal` | `@Column({ type: 'decimal', precision: n, scale: m })` | `@IsNumber()` | Precision/scale supported |
|
|
390
|
-
| `numeric` | `@Column({ type: 'numeric', precision: n, scale: m })` | `@IsNumber()` | Precision/scale supported |
|
|
391
|
-
| `timestamp` | `@Column({ type: 'timestamp' })` | None | |
|
|
392
|
-
| `point` | `@Column({ type: 'point' })` | None | PostGIS point geometry ⚠️ Requires PostGIS |
|
|
393
|
-
| `linestring` | `@Column({ type: 'linestring' })` | None | PostGIS line string geometry ⚠️ Requires PostGIS |
|
|
394
|
-
| `polygon` | `@Column({ type: 'polygon' })` | None | PostGIS polygon geometry ⚠️ Requires PostGIS |
|
|
395
|
-
| `multipoint` | `@Column({ type: 'multipoint' })` | None | PostGIS multi-point geometry ⚠️ Requires PostGIS |
|
|
396
|
-
| `multilinestring` | `@Column({ type: 'multilinestring' })` | None | PostGIS multi-line string geometry ⚠️ Requires PostGIS |
|
|
397
|
-
| `multipolygon` | `@Column({ type: 'multipolygon' })` | None | PostGIS multi-polygon geometry ⚠️ Requires PostGIS |
|
|
398
|
-
| `geometry` | `@Column({ type: 'geometry' })` | None | PostGIS generic geometry type ⚠️ Requires PostGIS |
|
|
399
|
-
| `geography` | `@Column({ type: 'geography' })` | None | PostGIS geography type ⚠️ Requires PostGIS |
|
|
400
|
-
| `geometrycollection` | `@Column({ type: 'geometrycollection' })` | None | PostGIS geometry collection ⚠️ Requires PostGIS |
|
|
401
|
-
|
|
402
|
-
#### Decimal/Numeric Field Examples
|
|
403
|
-
|
|
404
|
-
**Basic decimal field:**
|
|
405
|
-
```json
|
|
406
|
-
{
|
|
407
|
-
"name": "price",
|
|
408
|
-
"type": "decimal",
|
|
409
|
-
"precision": 10,
|
|
410
|
-
"scale": 2,
|
|
411
|
-
"default": 0,
|
|
412
|
-
"nullable": false
|
|
413
|
-
}
|
|
414
|
-
```
|
|
415
|
-
Generates:
|
|
416
|
-
```typescript
|
|
417
|
-
@Column({ "type": "decimal", precision: 10, scale: 2, default: 0 })
|
|
418
|
-
price: number;
|
|
419
|
-
```
|
|
38
|
+
## Quick start
|
|
420
39
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
"name": "bandwidth_gb",
|
|
425
|
-
"type": "numeric",
|
|
426
|
-
"precision": 10,
|
|
427
|
-
"scale": 3,
|
|
428
|
-
"default": 0,
|
|
429
|
-
"nullable": false
|
|
430
|
-
}
|
|
431
|
-
```
|
|
432
|
-
Generates:
|
|
433
|
-
```typescript
|
|
434
|
-
@Column({ "type": "numeric", precision: 10, scale: 3, default: 0 })
|
|
435
|
-
bandwidth_gb: number;
|
|
436
|
-
```
|
|
40
|
+
```bash
|
|
41
|
+
# Create a new project
|
|
42
|
+
apso init --name my-app --language typescript
|
|
437
43
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
> **📋 PostGIS Setup Required:** Before using any PostGIS data types, ensure your PostgreSQL database has the PostGIS extension installed:
|
|
441
|
-
> ```sql
|
|
442
|
-
> -- Install PostGIS extension
|
|
443
|
-
> CREATE EXTENSION IF NOT EXISTS postgis;
|
|
444
|
-
>
|
|
445
|
-
> -- Verify installation
|
|
446
|
-
> SELECT PostGIS_Version();
|
|
447
|
-
> ```
|
|
448
|
-
>
|
|
449
|
-
> **Deployment Note:** When deploying applications with PostGIS fields, ensure the PostGIS extension is available in your production database environment.
|
|
450
|
-
|
|
451
|
-
**Point geometry field:**
|
|
452
|
-
```json
|
|
453
|
-
{
|
|
454
|
-
"name": "location",
|
|
455
|
-
"type": "point",
|
|
456
|
-
"nullable": true
|
|
457
|
-
}
|
|
458
|
-
```
|
|
459
|
-
Generates:
|
|
460
|
-
```typescript
|
|
461
|
-
@Column({
|
|
462
|
-
"type": "point",
|
|
463
|
-
transformer: {
|
|
464
|
-
to: (point: {x: number, y: number} | null) => {
|
|
465
|
-
if (!point) return null;
|
|
466
|
-
return `(${point.x},${point.y})`;
|
|
467
|
-
},
|
|
468
|
-
from: (pgPoint: string | null) => {
|
|
469
|
-
if (!pgPoint) return null;
|
|
470
|
-
const [x, y] = pgPoint.substring(1, pgPoint.length - 1).split(',');
|
|
471
|
-
return { x: parseFloat(x), y: parseFloat(y) };
|
|
472
|
-
}
|
|
473
|
-
},
|
|
474
|
-
nullable: true
|
|
475
|
-
})
|
|
476
|
-
location: { x: number, y: number };
|
|
477
|
-
```
|
|
44
|
+
# Edit .apsorc to define your schema
|
|
478
45
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
{
|
|
482
|
-
"name": "boundary",
|
|
483
|
-
"type": "polygon",
|
|
484
|
-
"nullable": true
|
|
485
|
-
}
|
|
486
|
-
```
|
|
487
|
-
Generates:
|
|
488
|
-
```typescript
|
|
489
|
-
@Column({
|
|
490
|
-
"type": "polygon",
|
|
491
|
-
transformer: {
|
|
492
|
-
to: (polygon: { coordinates: Array<Array<{x: number, y: number}>> } | null) => {
|
|
493
|
-
if (!polygon) return null;
|
|
494
|
-
const rings = polygon.coordinates.map(ring => {
|
|
495
|
-
const coords = ring.map(coord => `${coord.x} ${coord.y}`).join(',');
|
|
496
|
-
return `(${coords})`;
|
|
497
|
-
});
|
|
498
|
-
return `POLYGON(${rings.join(',')})`;
|
|
499
|
-
},
|
|
500
|
-
from: (pgPolygon: string | null) => {
|
|
501
|
-
if (!pgPolygon) return null;
|
|
502
|
-
const match = pgPolygon.match(/POLYGON\((.+)\)/);
|
|
503
|
-
if (!match) return null;
|
|
504
|
-
const rings = match[1].split('),(').map(ring => {
|
|
505
|
-
const cleanRing = ring.replace(/[()]/g, '');
|
|
506
|
-
const coords = cleanRing.split(',').map(coord => {
|
|
507
|
-
const [x, y] = coord.trim().split(' ');
|
|
508
|
-
return { x: parseFloat(x), y: parseFloat(y) };
|
|
509
|
-
});
|
|
510
|
-
return coords;
|
|
511
|
-
});
|
|
512
|
-
return { coordinates: rings };
|
|
513
|
-
}
|
|
514
|
-
},
|
|
515
|
-
nullable: true
|
|
516
|
-
})
|
|
517
|
-
boundary: { coordinates: Array<Array<{ x: number, y: number }>> };
|
|
518
|
-
```
|
|
46
|
+
# Generate code from schema
|
|
47
|
+
apso generate
|
|
519
48
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
{
|
|
523
|
-
"name": "shape",
|
|
524
|
-
"type": "geometry",
|
|
525
|
-
"nullable": true
|
|
526
|
-
}
|
|
49
|
+
# Start Postgres and run the API
|
|
50
|
+
apso dev
|
|
527
51
|
```
|
|
528
|
-
Generates:
|
|
529
|
-
```typescript
|
|
530
|
-
@Column({
|
|
531
|
-
"type": "geometry",
|
|
532
|
-
transformer: {
|
|
533
|
-
to: (geometry: any) => {
|
|
534
|
-
if (!geometry) return null;
|
|
535
|
-
return geometry;
|
|
536
|
-
},
|
|
537
|
-
from: (pgGeometry: any) => {
|
|
538
|
-
if (!pgGeometry) return null;
|
|
539
|
-
return pgGeometry;
|
|
540
|
-
}
|
|
541
|
-
},
|
|
542
|
-
nullable: true
|
|
543
|
-
})
|
|
544
|
-
shape: any;
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
### 3. Validation Rules Documentation
|
|
548
52
|
|
|
549
|
-
|
|
53
|
+
Your API is live at `http://localhost:3000` with Swagger docs at `/api`. The generated code lives in `src/autogen/` and is standard NestJS with TypeORM -- no Apso runtime dependency.
|
|
550
54
|
|
|
551
|
-
|
|
552
|
-
- `"nullable": true` → `@Column({ nullable: true })` and `@IsOptional()`
|
|
553
|
-
- `"is_email": true` → `@IsEmail()`
|
|
554
|
-
- `"length": 255` → `@MaxLength(255)`
|
|
555
|
-
- `"precision": 10` → `@Column({ precision: 10 })` (for decimal/numeric types)
|
|
556
|
-
- `"scale": 2` → `@Column({ scale: 2 })` (for decimal/numeric types)
|
|
557
|
-
- `"required": false` → `@IsOptional()` (for CREATE group)
|
|
55
|
+
## Commands
|
|
558
56
|
|
|
559
|
-
|
|
57
|
+
| Command | Subcommands | Description |
|
|
58
|
+
|---------|------------|-------------|
|
|
59
|
+
| [init](#apso-init) | | Create a new project |
|
|
60
|
+
| [generate](#apso-generate) | | Generate code from `.apsorc` schema |
|
|
61
|
+
| [dev](#apso-dev) | | Start local dev server via Docker Compose |
|
|
62
|
+
| [migrate](#apso-migrate) | | Test schema migrations locally with PGlite |
|
|
63
|
+
| [deploy](#apso-deploy) | | Deploy to Apso platform |
|
|
64
|
+
| [login](#apso-login) | | Authenticate with Apso |
|
|
65
|
+
| [logout](#apso-logout) | | Clear stored credentials |
|
|
66
|
+
| [whoami](#apso-whoami) | | Show current user |
|
|
67
|
+
| [link](#apso-link) | | Link project to a platform service |
|
|
68
|
+
| [unlink](#apso-unlink) | | Remove platform link |
|
|
69
|
+
| [status](#apso-status) | | Show service and build status |
|
|
70
|
+
| [logs](#apso-logs) | | View build logs |
|
|
71
|
+
| [open](#apso-open) | | Open service dashboard in browser |
|
|
72
|
+
| [projects](#apso-projects) | | List services in a workspace |
|
|
73
|
+
| [config](#apso-config) | `get`, `set`, `reset` | View or modify CLI configuration |
|
|
74
|
+
| [schema](#apso-schema) | `diff`, `push`, `pull`, `validate` | Manage schema sync with platform |
|
|
560
75
|
|
|
561
|
-
|
|
76
|
+
## Command reference
|
|
562
77
|
|
|
563
|
-
|
|
564
|
-
{ "from": "User", "to": "Workspace", "type": "OneToMany", "to_name": "ownedWorkspaces" }
|
|
565
|
-
```
|
|
566
|
-
Generates:
|
|
567
|
-
```typescript
|
|
568
|
-
@OneToMany(() => Workspace, (workspace) => workspace.user)
|
|
569
|
-
ownedWorkspaces: Workspace[];
|
|
570
|
-
```
|
|
78
|
+
### `apso init`
|
|
571
79
|
|
|
572
|
-
|
|
80
|
+
Create a new Apso project from a language-specific template.
|
|
573
81
|
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
```typescript
|
|
579
|
-
@ManyToOne(() => User, (user) => user.ownedWorkspaces)
|
|
580
|
-
@JoinColumn({ name: 'userId' })
|
|
581
|
-
owner: User;
|
|
582
|
-
|
|
583
|
-
@Column({ type: 'integer' })
|
|
584
|
-
userId: number;
|
|
82
|
+
```bash
|
|
83
|
+
apso init
|
|
84
|
+
apso init --name my-app --language typescript
|
|
85
|
+
apso init --name my-app --language python --skip-platform
|
|
585
86
|
```
|
|
586
87
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
- Entity class names remain as defined in `.apsorc` (e.g., `User`).
|
|
593
|
-
- Table names are lowercased (e.g., `user`).
|
|
594
|
-
- Foreign key columns use camelCase + `Id` (e.g., `userId`).
|
|
595
|
-
- Join column names match the foreign key column name.
|
|
596
|
-
|
|
597
|
-
### 6. Version 2 Format Differences
|
|
598
|
-
|
|
599
|
-
Version 2 of the `.apsorc` format introduces several enhancements:
|
|
600
|
-
- Separate `relationships` and `entities` arrays.
|
|
601
|
-
- `created_at`/`updated_at` as boolean flags at the entity level.
|
|
602
|
-
- Enhanced field types: `json`, `enum`, `timestamp`, `decimal`, `numeric`.
|
|
603
|
-
- `to_name` property in relationships for custom property names.
|
|
604
|
-
- `precision` and `scale` properties for decimal/numeric fields.
|
|
605
|
-
|
|
606
|
-
Refer to the [example v2 file](#example-apsorc-v2-file) for usage.
|
|
607
|
-
|
|
608
|
-
## Relationships
|
|
88
|
+
| Option | Description | Default |
|
|
89
|
+
|--------|-------------|---------|
|
|
90
|
+
| `-n, --name` | Project name | _(prompted)_ |
|
|
91
|
+
| `-l, --language` | Target language (`typescript`, `python`, `go`) | _(prompted)_ |
|
|
92
|
+
| `--skip-platform` | Skip platform linking (offline mode) | `false` |
|
|
609
93
|
|
|
610
|
-
|
|
94
|
+
When authenticated, `apso init` lets you create a new project or clone an existing one from the platform.
|
|
611
95
|
|
|
612
|
-
|
|
96
|
+
### `apso generate`
|
|
613
97
|
|
|
614
|
-
|
|
98
|
+
Generate backend code from the `.apsorc` schema file.
|
|
615
99
|
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
// ✅ CORRECT - One Side Only:
|
|
622
|
-
{ "from": "Workspace", "to": "User", "type": "ManyToOne", "to_name": "owner" }
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
- **DO:** Define only the direction that best fits your domain model.
|
|
626
|
-
- **DON'T:** Define both directions for the same relationship.
|
|
627
|
-
- **Why?** Apso CLI auto-generates the inverse property and decorators. Duplicating both sides causes duplicate property and foreign key generation.
|
|
628
|
-
|
|
629
|
-
### 2. Auto-Generated Properties Documentation
|
|
630
|
-
|
|
631
|
-
#### ManyToOne Relationships
|
|
632
|
-
- Generates:
|
|
633
|
-
- Entity property with `@ManyToOne`, `@JoinColumn`, and `@Column` decorators
|
|
634
|
-
- Foreign key column: `{relationName}Id`
|
|
635
|
-
- Example:
|
|
636
|
-
```typescript
|
|
637
|
-
@ManyToOne(() => User)
|
|
638
|
-
@JoinColumn({ name: 'ownerId' })
|
|
639
|
-
owner: User;
|
|
640
|
-
|
|
641
|
-
@Column({ type: 'integer' })
|
|
642
|
-
ownerId: number;
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
#### OneToMany Relationships
|
|
646
|
-
- Generates:
|
|
647
|
-
- Array property with `@OneToMany` decorator
|
|
648
|
-
- Inverse mapping to the ManyToOne property
|
|
649
|
-
- Example:
|
|
650
|
-
```typescript
|
|
651
|
-
@OneToMany(() => Workspace, (workspace) => workspace.owner)
|
|
652
|
-
workspaces: Workspace[];
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
#### ManyToMany Relationships
|
|
656
|
-
- Generates:
|
|
657
|
-
- Join table and array properties on both entities
|
|
658
|
-
- Example:
|
|
659
|
-
```typescript
|
|
660
|
-
@ManyToMany(() => Tag, (tag) => tag.posts)
|
|
661
|
-
@JoinTable()
|
|
662
|
-
tags: Tag[];
|
|
663
|
-
```
|
|
664
|
-
- **Warning:** If you define both a ManyToMany relationship and an explicit join entity, you may get duplicate join tables and properties. Prefer one approach.
|
|
665
|
-
|
|
666
|
-
### 3. Common Pitfalls and Solutions
|
|
667
|
-
|
|
668
|
-
#### Duplicate Identifier Errors
|
|
669
|
-
- **Cause:** Defining both sides of a relationship (bidirectional definitions)
|
|
670
|
-
- **Solution:** Remove one side; only define the relationship once.
|
|
671
|
-
|
|
672
|
-
#### "Property does not exist" Errors
|
|
673
|
-
- **Cause:** Referencing an inverse property that was not generated (e.g., using a `to_name` that doesn't match)
|
|
674
|
-
- **Solution:** Only use explicitly defined property names; check generated code for actual property names.
|
|
675
|
-
|
|
676
|
-
#### "Multiple properties with same name" Errors
|
|
677
|
-
- **Cause:** Complex or circular relationship chains, or duplicate relationship definitions
|
|
678
|
-
- **Solution:** Simplify relationships, avoid deep nesting, and ensure each relationship is defined only once.
|
|
679
|
-
|
|
680
|
-
#### ManyToMany + Explicit Join Entity Conflicts
|
|
681
|
-
- **Cause:** Defining both a ManyToMany and a join entity for the same relationship
|
|
682
|
-
- **Solution:** Use either a ManyToMany or a join entity, not both.
|
|
683
|
-
|
|
684
|
-
#### Circular Eager Loading Issues
|
|
685
|
-
- **Cause:** Deeply nested or circular relationships
|
|
686
|
-
- **Solution:** Limit eager loading, use lazy relations, and avoid unnecessary deep nesting in your model.
|
|
687
|
-
|
|
688
|
-
### 4. Relationship Patterns
|
|
689
|
-
|
|
690
|
-
#### Multi-Tenant Architecture
|
|
691
|
-
```json
|
|
692
|
-
{ "from": "Resource", "to": "Workspace", "type": "ManyToOne", "to_name": "workspace" },
|
|
693
|
-
{ "from": "Resource", "to": "User", "type": "ManyToOne", "to_name": "createdBy" }
|
|
694
|
-
```
|
|
695
|
-
|
|
696
|
-
#### Audit Trails, Deployment History, Optional Relationships
|
|
697
|
-
```json
|
|
698
|
-
{ "from": "Deployment", "to": "User", "type": "ManyToOne", "to_name": "deployedBy", "nullable": true },
|
|
699
|
-
{ "from": "AuditLog", "to": "Resource", "type": "ManyToOne", "to_name": "resource" }
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
#### ManyToMany Example
|
|
703
|
-
```json
|
|
704
|
-
{ "from": "User", "to": "Role", "type": "ManyToMany", "to_name": "roles" }
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
### 5. Testing and Validation Guide
|
|
708
|
-
|
|
709
|
-
- **Validate Relationship Generation:**
|
|
710
|
-
- After running `apso server scaffold`, inspect the generated entity files in the `autogen` directory (never modify these directly—see above for extension instructions).
|
|
711
|
-
- Check that only one property exists for each relationship per entity.
|
|
712
|
-
- Confirm that foreign key columns and decorators are present as expected.
|
|
713
|
-
|
|
714
|
-
- **Build Testing Procedures:**
|
|
715
|
-
- Run `tsc` or your build process to catch duplicate or missing property errors early.
|
|
716
|
-
- Write unit tests for entity relationships if possible.
|
|
717
|
-
|
|
718
|
-
- **Entity Inspection Checklist:**
|
|
719
|
-
- No duplicate properties or foreign keys
|
|
720
|
-
- All relationships have the correct decorators
|
|
721
|
-
- No circular imports or eager loading loops
|
|
722
|
-
|
|
723
|
-
- **Clean Regeneration Practices:**
|
|
724
|
-
- Before re-scaffolding, remove old generated code:
|
|
725
|
-
```sh
|
|
726
|
-
rm -rf autogen
|
|
727
|
-
apso server scaffold
|
|
728
|
-
```
|
|
729
|
-
This prevents stale or duplicate files from causing errors. **Never add custom code to autogen—use extensions as described above.**
|
|
730
|
-
|
|
731
|
-
### 6. Version 2 Format Clarity
|
|
732
|
-
|
|
733
|
-
- In v2, relationships and entities are defined in separate arrays:
|
|
734
|
-
```json
|
|
735
|
-
{
|
|
736
|
-
"version": 2,
|
|
737
|
-
"entities": [ ... ],
|
|
738
|
-
"relationships": [ ... ]
|
|
739
|
-
}
|
|
740
|
-
```
|
|
741
|
-
- Each relationship should only be defined once, with clear `to_name` if you want a custom property name.
|
|
742
|
-
- Field types and validation are specified per the [Auto-Generated Code Reference](#auto-generated-code-reference).
|
|
743
|
-
|
|
744
|
-
> **Summary:**
|
|
745
|
-
>
|
|
746
|
-
> - Only define one side of each relationship in `.apsorc`.
|
|
747
|
-
> - Let Apso CLI auto-generate the inverse side.
|
|
748
|
-
> - Avoid deep nesting and duplicate definitions.
|
|
749
|
-
> - Always inspect generated code and test your build after scaffolding.
|
|
750
|
-
|
|
751
|
-
## Authentication (Bring Your Own Auth)
|
|
752
|
-
|
|
753
|
-
Apso provides flexible, provider-agnostic authentication that generates NestJS guards from your `.apsorc` configuration. Unlike monolithic platforms that force vendor lock-in through proprietary auth systems, Apso embraces **code ownership** - you choose your auth provider, and you own the generated code.
|
|
754
|
-
|
|
755
|
-
### Philosophy: Why Bring Your Own Auth Matters
|
|
756
|
-
|
|
757
|
-
Traditional BaaS platforms like Supabase and Firebase provide authentication as a core feature, but this creates dependency:
|
|
758
|
-
- Your user data lives in their systems
|
|
759
|
-
- Migrating away requires rewriting auth logic
|
|
760
|
-
- You're bound to their pricing, features, and roadmap
|
|
761
|
-
|
|
762
|
-
**Apso takes a different approach:**
|
|
763
|
-
- **Provider flexibility** - Use Better Auth, Auth0, Cognito, Clerk, or custom solutions
|
|
764
|
-
- **Code ownership** - Generated guards are standard NestJS code you can inspect, modify, and extend
|
|
765
|
-
- **Zero lock-in** - Switch providers by changing configuration, not rewriting code
|
|
766
|
-
- **Normalized interface** - All providers produce the same `AuthContext` consumed by scoping and RBAC
|
|
767
|
-
|
|
768
|
-
This philosophy ensures your authentication layer is **timeless** - it grows with your needs and migrates with your stack.
|
|
769
|
-
|
|
770
|
-
### Supported Authentication Providers
|
|
771
|
-
|
|
772
|
-
| Provider | Type | Best For |
|
|
773
|
-
|----------|------|----------|
|
|
774
|
-
| `better-auth` | Database Sessions | Self-hosted apps, maximum control |
|
|
775
|
-
| `custom-db-session` | Database Sessions | Existing session tables, custom flows |
|
|
776
|
-
| `auth0` | JWT | Enterprise SSO, social login |
|
|
777
|
-
| `clerk` | JWT | Modern SaaS with prebuilt UI |
|
|
778
|
-
| `cognito` | JWT | AWS ecosystem integration |
|
|
779
|
-
| `api-key` | API Keys | Service-to-service auth, public APIs |
|
|
780
|
-
|
|
781
|
-
### Basic Configuration
|
|
782
|
-
|
|
783
|
-
Add an `auth` block to your `.apsorc`:
|
|
784
|
-
|
|
785
|
-
```json
|
|
786
|
-
{
|
|
787
|
-
"version": 2,
|
|
788
|
-
"auth": {
|
|
789
|
-
"provider": "better-auth"
|
|
790
|
-
},
|
|
791
|
-
"entities": [...]
|
|
792
|
-
}
|
|
100
|
+
```bash
|
|
101
|
+
apso generate
|
|
102
|
+
apso generate --language python
|
|
103
|
+
apso generate --skip-format
|
|
793
104
|
```
|
|
794
105
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
106
|
+
| Option | Description | Default |
|
|
107
|
+
|--------|-------------|---------|
|
|
108
|
+
| `-l, --language` | Target language (`typescript`, `python`, `go`) | _(from .apsorc or prompted)_ |
|
|
109
|
+
| `--skip-format` | Skip Prettier formatting after generation | `false` |
|
|
798
110
|
|
|
799
|
-
|
|
111
|
+
Generated files are placed in `src/autogen/`. These files are overwritten on each run. Place custom code in `src/extensions/` to avoid losing changes.
|
|
800
112
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
```json
|
|
804
|
-
{
|
|
805
|
-
"auth": {
|
|
806
|
-
"provider": "better-auth",
|
|
807
|
-
"sessionEntity": "session",
|
|
808
|
-
"userEntity": "User",
|
|
809
|
-
"accountUserEntity": "AccountUser",
|
|
810
|
-
"cookiePrefix": "myapp",
|
|
811
|
-
"organizationField": "organizationId",
|
|
812
|
-
"roleField": "role"
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
```
|
|
113
|
+
### `apso dev`
|
|
816
114
|
|
|
817
|
-
|
|
818
|
-
|--------|---------|-------------|
|
|
819
|
-
| `sessionEntity` | `"session"` | Entity storing session tokens |
|
|
820
|
-
| `userEntity` | `"User"` | Entity for user accounts |
|
|
821
|
-
| `accountUserEntity` | `"AccountUser"` | Junction entity for user-org mapping |
|
|
822
|
-
| `cookiePrefix` | service name | Cookie name prefix (e.g., `myapp.session_token`) |
|
|
823
|
-
| `organizationField` | `"organizationId"` | Field on accountUserEntity for org ID |
|
|
824
|
-
| `roleField` | `"role"` | Field on accountUserEntity for user role |
|
|
825
|
-
|
|
826
|
-
#### JWT Providers (Auth0, Clerk, Cognito)
|
|
827
|
-
|
|
828
|
-
For JWT-based authentication:
|
|
829
|
-
|
|
830
|
-
```json
|
|
831
|
-
{
|
|
832
|
-
"auth": {
|
|
833
|
-
"provider": "auth0",
|
|
834
|
-
"jwt": {
|
|
835
|
-
"issuer": "https://your-tenant.auth0.com/",
|
|
836
|
-
"audience": "https://your-api.example.com",
|
|
837
|
-
"jwksUri": "https://your-tenant.auth0.com/.well-known/jwks.json",
|
|
838
|
-
"algorithms": ["RS256"]
|
|
839
|
-
},
|
|
840
|
-
"claims": {
|
|
841
|
-
"userId": "sub",
|
|
842
|
-
"email": "email",
|
|
843
|
-
"organizationId": "org_id",
|
|
844
|
-
"roles": "permissions"
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
```
|
|
115
|
+
Start the local development server using Docker Compose.
|
|
849
116
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
| `jwksUri` | `issuer + /.well-known/jwks.json` | JWKS endpoint for key rotation |
|
|
855
|
-
| `algorithms` | `["RS256"]` | Accepted signing algorithms |
|
|
856
|
-
|
|
857
|
-
| Claims Option | Default | Description |
|
|
858
|
-
|---------------|---------|-------------|
|
|
859
|
-
| `userId` | `"sub"` | Claim containing user ID |
|
|
860
|
-
| `email` | `"email"` | Claim containing user email |
|
|
861
|
-
| `organizationId` | - | Claim for org/workspace ID |
|
|
862
|
-
| `roles` | `"roles"` | Claim containing role array |
|
|
863
|
-
|
|
864
|
-
#### API Key Authentication
|
|
865
|
-
|
|
866
|
-
For service-to-service or public API authentication:
|
|
867
|
-
|
|
868
|
-
```json
|
|
869
|
-
{
|
|
870
|
-
"auth": {
|
|
871
|
-
"provider": "api-key",
|
|
872
|
-
"apiKeyHeader": "x-api-key",
|
|
873
|
-
"apiKeyEntity": "ApiKey",
|
|
874
|
-
"workspaceField": "workspaceId",
|
|
875
|
-
"roleField": "permissions"
|
|
876
|
-
}
|
|
877
|
-
}
|
|
117
|
+
```bash
|
|
118
|
+
apso dev
|
|
119
|
+
apso dev --build
|
|
120
|
+
apso dev --detach
|
|
878
121
|
```
|
|
879
122
|
|
|
880
|
-
| Option |
|
|
881
|
-
|
|
882
|
-
| `
|
|
883
|
-
| `
|
|
884
|
-
| `workspaceField` | - | Field on entity for workspace ID |
|
|
885
|
-
| `roleField` | - | Field on entity for permissions |
|
|
886
|
-
|
|
887
|
-
### The AuthContext Interface
|
|
888
|
-
|
|
889
|
-
All auth providers produce a normalized `AuthContext` that's attached to every authenticated request:
|
|
890
|
-
|
|
891
|
-
```typescript
|
|
892
|
-
interface AuthContext {
|
|
893
|
-
userId?: string; // The authenticated user's ID
|
|
894
|
-
email?: string; // User's email (if available)
|
|
895
|
-
workspaceId?: string; // Workspace/tenant ID
|
|
896
|
-
organizationId?: string; // Organization ID (alias)
|
|
897
|
-
roles: string[]; // User's roles/permissions
|
|
898
|
-
serviceId?: string; // For API key auth: the key identifier
|
|
899
|
-
user?: unknown; // Raw user object (provider-specific)
|
|
900
|
-
session?: unknown; // Raw session object (provider-specific)
|
|
901
|
-
}
|
|
902
|
-
```
|
|
123
|
+
| Option | Description | Default |
|
|
124
|
+
|--------|-------------|---------|
|
|
125
|
+
| `--build` | Rebuild images before starting | `false` |
|
|
126
|
+
| `-d, --detach` | Run containers in the background | `false` |
|
|
903
127
|
|
|
904
|
-
|
|
128
|
+
Requires Docker and Docker Compose. Looks for `docker-compose.yml` in the current directory.
|
|
905
129
|
|
|
906
|
-
###
|
|
130
|
+
### `apso migrate`
|
|
907
131
|
|
|
908
|
-
|
|
132
|
+
Detect schema changes, generate migration SQL, and test against a local PGlite sandbox. No Docker or external database required.
|
|
909
133
|
|
|
134
|
+
```bash
|
|
135
|
+
apso migrate # Detect changes, generate and test SQL
|
|
136
|
+
apso migrate --apply # Update snapshot after successful test
|
|
137
|
+
apso migrate --reset # Clear sandbox and start fresh
|
|
138
|
+
apso migrate --sql # Output raw SQL only (for piping)
|
|
910
139
|
```
|
|
911
|
-
src/
|
|
912
|
-
guards/
|
|
913
|
-
auth.guard.ts # Authentication guard implementation
|
|
914
|
-
scope.guard.ts # Data scoping guard (if scopeBy used)
|
|
915
|
-
guards.module.ts # NestJS module with providers
|
|
916
|
-
index.ts # Exports
|
|
917
|
-
```
|
|
918
|
-
|
|
919
|
-
### Enabling Authentication
|
|
920
|
-
|
|
921
|
-
Guards are generated but **not enabled globally by default**. Enable them based on your needs:
|
|
922
140
|
|
|
923
|
-
|
|
141
|
+
| Option | Description | Default |
|
|
142
|
+
|--------|-------------|---------|
|
|
143
|
+
| `--apply` | Update local schema snapshot after verified migration | `false` |
|
|
144
|
+
| `--reset` | Reset the sandbox (clear snapshot and PGlite data) | `false` |
|
|
145
|
+
| `--sql` | Output raw SQL statements only | `false` |
|
|
924
146
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
```typescript
|
|
928
|
-
providers: [
|
|
929
|
-
AuthGuard,
|
|
930
|
-
ScopeGuard,
|
|
931
|
-
// Uncomment to enable globally:
|
|
932
|
-
{
|
|
933
|
-
provide: APP_GUARD,
|
|
934
|
-
useClass: AuthGuard,
|
|
935
|
-
},
|
|
936
|
-
{
|
|
937
|
-
provide: APP_GUARD,
|
|
938
|
-
useClass: ScopeGuard,
|
|
939
|
-
},
|
|
940
|
-
],
|
|
941
|
-
```
|
|
147
|
+
The sandbox works by comparing your current `.apsorc` against the last-known snapshot, generating the migration SQL, and executing it against an in-process Postgres instance (PGlite). If the migration fails locally, you know before it reaches any real database.
|
|
942
148
|
|
|
943
|
-
|
|
149
|
+
### `apso deploy`
|
|
944
150
|
|
|
945
|
-
|
|
946
|
-
import { AuthGuard } from '../guards';
|
|
151
|
+
Deploy the linked service to the Apso platform. Runs a local migration check before deploying.
|
|
947
152
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
153
|
+
```bash
|
|
154
|
+
apso deploy
|
|
155
|
+
apso deploy --yes
|
|
156
|
+
apso deploy --skip-migrate
|
|
157
|
+
apso deploy --no-wait
|
|
951
158
|
```
|
|
952
159
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
getProfile(@Req() req: AuthenticatedRequest) {
|
|
959
|
-
return req.auth;
|
|
960
|
-
}
|
|
961
|
-
```
|
|
160
|
+
| Option | Description | Default |
|
|
161
|
+
|--------|-------------|---------|
|
|
162
|
+
| `-y, --yes` | Skip confirmation prompt | `false` |
|
|
163
|
+
| `--skip-migrate` | Skip local migration validation | `false` |
|
|
164
|
+
| `--no-wait` | Trigger deploy without waiting for completion | `false` |
|
|
962
165
|
|
|
963
|
-
|
|
166
|
+
If schema changes are detected, `apso deploy` shows the migration SQL and asks for confirmation before proceeding. If the migration fails locally, the deploy is blocked.
|
|
964
167
|
|
|
965
|
-
|
|
966
|
-
import { Public, SkipScopeCheck } from './guards';
|
|
168
|
+
### `apso login`
|
|
967
169
|
|
|
968
|
-
|
|
969
|
-
@Public()
|
|
970
|
-
@Get('health')
|
|
971
|
-
healthCheck() { }
|
|
170
|
+
Authenticate with the Apso platform via browser-based OAuth or API token.
|
|
972
171
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
adminStats() { }
|
|
172
|
+
```bash
|
|
173
|
+
apso login
|
|
174
|
+
apso login --token <api-token>
|
|
977
175
|
```
|
|
978
176
|
|
|
979
|
-
|
|
177
|
+
| Option | Description |
|
|
178
|
+
|--------|-------------|
|
|
179
|
+
| `-t, --token` | API token for non-interactive login (CI/CD) |
|
|
980
180
|
|
|
981
|
-
|
|
181
|
+
### `apso logout`
|
|
982
182
|
|
|
983
|
-
|
|
984
|
-
import { AuthenticatedRequest, getAuthContext, requireAuthContext } from './guards';
|
|
183
|
+
Clear stored credentials.
|
|
985
184
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
@Get()
|
|
989
|
-
findAll(@Req() req: AuthenticatedRequest) {
|
|
990
|
-
// Direct access
|
|
991
|
-
const userId = req.auth.userId;
|
|
992
|
-
const orgId = req.auth.organizationId;
|
|
993
|
-
|
|
994
|
-
// Or use helpers
|
|
995
|
-
const ctx = requireAuthContext(req); // Throws if not authenticated
|
|
996
|
-
return this.projectService.findByOrg(ctx.organizationId);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
```
|
|
1000
|
-
|
|
1001
|
-
### Token Extraction
|
|
1002
|
-
|
|
1003
|
-
The generated auth guard extracts tokens from multiple locations (in order):
|
|
1004
|
-
|
|
1005
|
-
1. **Authorization header**: `Bearer <token>`
|
|
1006
|
-
2. **Cookies**: `{cookiePrefix}.session_token`, `better-auth.session_token`, or `session_token`
|
|
1007
|
-
3. **Custom header**: `X-Session-Token`
|
|
1008
|
-
|
|
1009
|
-
This flexibility supports both browser-based apps (cookies) and API clients (headers).
|
|
1010
|
-
|
|
1011
|
-
### Complete Example
|
|
1012
|
-
|
|
1013
|
-
```json
|
|
1014
|
-
{
|
|
1015
|
-
"version": 2,
|
|
1016
|
-
"auth": {
|
|
1017
|
-
"provider": "better-auth",
|
|
1018
|
-
"sessionEntity": "session",
|
|
1019
|
-
"userEntity": "User",
|
|
1020
|
-
"accountUserEntity": "AccountUser",
|
|
1021
|
-
"cookiePrefix": "myapp",
|
|
1022
|
-
"organizationField": "organizationId",
|
|
1023
|
-
"roleField": "role"
|
|
1024
|
-
},
|
|
1025
|
-
"entities": [
|
|
1026
|
-
{
|
|
1027
|
-
"name": "User",
|
|
1028
|
-
"fields": [
|
|
1029
|
-
{ "name": "email", "type": "text", "unique": true },
|
|
1030
|
-
{ "name": "name", "type": "text", "nullable": true }
|
|
1031
|
-
]
|
|
1032
|
-
},
|
|
1033
|
-
{
|
|
1034
|
-
"name": "session",
|
|
1035
|
-
"fields": [
|
|
1036
|
-
{ "name": "token", "type": "text", "unique": true },
|
|
1037
|
-
{ "name": "expiresAt", "type": "timestamp" },
|
|
1038
|
-
{ "name": "userId", "type": "text" }
|
|
1039
|
-
]
|
|
1040
|
-
},
|
|
1041
|
-
{
|
|
1042
|
-
"name": "Organization",
|
|
1043
|
-
"fields": [
|
|
1044
|
-
{ "name": "name", "type": "text" }
|
|
1045
|
-
]
|
|
1046
|
-
},
|
|
1047
|
-
{
|
|
1048
|
-
"name": "AccountUser",
|
|
1049
|
-
"fields": [
|
|
1050
|
-
{ "name": "role", "type": "enum", "values": ["owner", "admin", "member"] }
|
|
1051
|
-
]
|
|
1052
|
-
},
|
|
1053
|
-
{
|
|
1054
|
-
"name": "Project",
|
|
1055
|
-
"scopeBy": "organizationId",
|
|
1056
|
-
"fields": [
|
|
1057
|
-
{ "name": "name", "type": "text" }
|
|
1058
|
-
]
|
|
1059
|
-
}
|
|
1060
|
-
],
|
|
1061
|
-
"relationships": [
|
|
1062
|
-
{ "from": "AccountUser", "to": "User", "type": "ManyToOne" },
|
|
1063
|
-
{ "from": "AccountUser", "to": "Organization", "type": "ManyToOne" },
|
|
1064
|
-
{ "from": "Project", "to": "Organization", "type": "ManyToOne" }
|
|
1065
|
-
]
|
|
1066
|
-
}
|
|
185
|
+
```bash
|
|
186
|
+
apso logout
|
|
1067
187
|
```
|
|
1068
188
|
|
|
1069
|
-
###
|
|
1070
|
-
|
|
1071
|
-
One of Apso's key advantages is seamless provider migration:
|
|
1072
|
-
|
|
1073
|
-
1. Update the `auth` block in `.apsorc`
|
|
1074
|
-
2. Run `apso server scaffold`
|
|
1075
|
-
3. Update your frontend to use the new provider's login flow
|
|
1076
|
-
|
|
1077
|
-
Your business logic remains unchanged because it only interacts with the normalized `AuthContext`.
|
|
1078
|
-
|
|
1079
|
-
---
|
|
189
|
+
### `apso whoami`
|
|
1080
190
|
|
|
1081
|
-
|
|
191
|
+
Display information about the current authenticated user and linked project.
|
|
1082
192
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
### Philosophy: Application-Layer RLS
|
|
1086
|
-
|
|
1087
|
-
Traditional approaches to multi-tenant data isolation include:
|
|
1088
|
-
- **Database RLS (Supabase)** - Powerful but opaque, tied to PostgreSQL, difficult to debug
|
|
1089
|
-
- **Manual filtering** - Error-prone, repetitive, easy to forget on new endpoints
|
|
1090
|
-
- **ORM middleware** - Often complex, hard to customize
|
|
1091
|
-
|
|
1092
|
-
**Apso's approach delivers the best of all worlds:**
|
|
1093
|
-
- **Declarative** - Define scope once in `.apsorc`, applied everywhere
|
|
1094
|
-
- **Transparent** - Generated guards are standard NestJS code you can inspect and debug
|
|
1095
|
-
- **Portable** - Works with any database, not locked to PostgreSQL RLS
|
|
1096
|
-
- **Flexible** - Configure per-entity behavior: auto-injection, filtering, bypass rules
|
|
1097
|
-
|
|
1098
|
-
This design is **timeless** - your data isolation logic is explicit code, not hidden database magic.
|
|
1099
|
-
|
|
1100
|
-
### What is scopeBy?
|
|
1101
|
-
|
|
1102
|
-
The `scopeBy` property on entities defines which field(s) determine the authorization scope for that entity. When configured, Apso generates NestJS guards that:
|
|
1103
|
-
|
|
1104
|
-
- **Auto-inject** scope values on create operations (POST requests)
|
|
1105
|
-
- **Auto-filter** queries by scope values on list operations (GET without ID)
|
|
1106
|
-
- **Verify ownership** on single-resource operations (GET/PUT/PATCH/DELETE by ID)
|
|
1107
|
-
|
|
1108
|
-
This is Apso's answer to PostgreSQL Row-Level Security (RLS), implemented at the application layer for flexibility and visibility.
|
|
1109
|
-
|
|
1110
|
-
### Basic Example
|
|
1111
|
-
|
|
1112
|
-
```json
|
|
1113
|
-
{
|
|
1114
|
-
"version": 2,
|
|
1115
|
-
"entities": [
|
|
1116
|
-
{
|
|
1117
|
-
"name": "Project",
|
|
1118
|
-
"scopeBy": "workspaceId",
|
|
1119
|
-
"fields": [
|
|
1120
|
-
{ "name": "name", "type": "text" }
|
|
1121
|
-
]
|
|
1122
|
-
}
|
|
1123
|
-
]
|
|
1124
|
-
}
|
|
193
|
+
```bash
|
|
194
|
+
apso whoami
|
|
1125
195
|
```
|
|
1126
196
|
|
|
1127
|
-
|
|
1128
|
-
- All Project queries filter by `workspaceId` from the request context
|
|
1129
|
-
- New Projects automatically get the `workspaceId` injected
|
|
1130
|
-
- Single Project access verifies the Project belongs to the user's workspace
|
|
1131
|
-
|
|
1132
|
-
### scopeBy Configuration Options
|
|
1133
|
-
|
|
1134
|
-
#### Single Field Scoping
|
|
1135
|
-
```json
|
|
1136
|
-
{
|
|
1137
|
-
"name": "Project",
|
|
1138
|
-
"scopeBy": "workspaceId"
|
|
1139
|
-
}
|
|
1140
|
-
```
|
|
197
|
+
### `apso link`
|
|
1141
198
|
|
|
1142
|
-
|
|
1143
|
-
```json
|
|
1144
|
-
{
|
|
1145
|
-
"name": "Task",
|
|
1146
|
-
"scopeBy": ["workspaceId", "projectId"]
|
|
1147
|
-
}
|
|
1148
|
-
```
|
|
199
|
+
Link the current project to a platform service.
|
|
1149
200
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
"name": "Comment",
|
|
1155
|
-
"scopeBy": "task.workspaceId"
|
|
1156
|
-
}
|
|
1157
|
-
```
|
|
1158
|
-
This tells the guard to look up the Task relationship and verify the workspaceId through that path.
|
|
1159
|
-
|
|
1160
|
-
### scopeOptions
|
|
1161
|
-
|
|
1162
|
-
Fine-tune scoping behavior with `scopeOptions`:
|
|
1163
|
-
|
|
1164
|
-
```json
|
|
1165
|
-
{
|
|
1166
|
-
"name": "AuditLog",
|
|
1167
|
-
"scopeBy": "workspaceId",
|
|
1168
|
-
"scopeOptions": {
|
|
1169
|
-
"injectOnCreate": false,
|
|
1170
|
-
"enforceOn": ["find", "get"],
|
|
1171
|
-
"bypassRoles": ["admin", "superadmin"]
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
201
|
+
```bash
|
|
202
|
+
apso link
|
|
203
|
+
apso link --workspace my-team --service my-api
|
|
204
|
+
apso link --force
|
|
1174
205
|
```
|
|
1175
206
|
|
|
1176
|
-
| Option |
|
|
1177
|
-
|
|
1178
|
-
|
|
|
1179
|
-
|
|
|
1180
|
-
|
|
|
207
|
+
| Option | Description |
|
|
208
|
+
|--------|-------------|
|
|
209
|
+
| `-w, --workspace` | Workspace slug |
|
|
210
|
+
| `-s, --service` | Service slug |
|
|
211
|
+
| `-f, --force` | Overwrite existing link without confirmation |
|
|
1181
212
|
|
|
1182
|
-
###
|
|
213
|
+
### `apso unlink`
|
|
1183
214
|
|
|
1184
|
-
|
|
215
|
+
Remove the link between the current project and the platform.
|
|
1185
216
|
|
|
217
|
+
```bash
|
|
218
|
+
apso unlink
|
|
1186
219
|
```
|
|
1187
|
-
src/
|
|
1188
|
-
guards/
|
|
1189
|
-
scope.guard.ts # Main guard implementation
|
|
1190
|
-
guards.module.ts # NestJS module with providers
|
|
1191
|
-
index.ts # Exports
|
|
1192
|
-
```
|
|
1193
|
-
|
|
1194
|
-
### Enabling Guards
|
|
1195
220
|
|
|
1196
|
-
|
|
221
|
+
### `apso status`
|
|
1197
222
|
|
|
1198
|
-
|
|
1199
|
-
Uncomment the APP_GUARD provider in `src/guards/guards.module.ts`:
|
|
223
|
+
Show the current service and latest build status.
|
|
1200
224
|
|
|
1201
|
-
```
|
|
1202
|
-
|
|
1203
|
-
ScopeGuard,
|
|
1204
|
-
// Uncomment to enable globally:
|
|
1205
|
-
{
|
|
1206
|
-
provide: APP_GUARD,
|
|
1207
|
-
useClass: ScopeGuard,
|
|
1208
|
-
},
|
|
1209
|
-
],
|
|
225
|
+
```bash
|
|
226
|
+
apso status
|
|
1210
227
|
```
|
|
1211
228
|
|
|
1212
|
-
|
|
1213
|
-
Apply to specific controllers:
|
|
229
|
+
### `apso logs`
|
|
1214
230
|
|
|
1215
|
-
|
|
1216
|
-
import { ScopeGuard } from '../guards';
|
|
231
|
+
View build logs for the linked service.
|
|
1217
232
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
```
|
|
1222
|
-
|
|
1223
|
-
#### Option 3: Per-Route Enable
|
|
1224
|
-
Apply to specific routes:
|
|
1225
|
-
|
|
1226
|
-
```typescript
|
|
1227
|
-
@UseGuards(ScopeGuard)
|
|
1228
|
-
@Get(':id')
|
|
1229
|
-
findOne(@Param('id') id: string) { }
|
|
233
|
+
```bash
|
|
234
|
+
apso logs
|
|
235
|
+
apso logs <build-id>
|
|
1230
236
|
```
|
|
1231
237
|
|
|
1232
|
-
###
|
|
1233
|
-
|
|
1234
|
-
The generated guard supports these decorators:
|
|
238
|
+
### `apso open`
|
|
1235
239
|
|
|
1236
|
-
|
|
1237
|
-
import { Public, SkipScopeCheck } from './guards';
|
|
240
|
+
Open the service dashboard or API endpoint in the browser.
|
|
1238
241
|
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
publicRoute() { }
|
|
1242
|
-
|
|
1243
|
-
@SkipScopeCheck() // Skip only scope checking (other guards still run)
|
|
1244
|
-
@Get('admin-dashboard')
|
|
1245
|
-
adminRoute() { }
|
|
242
|
+
```bash
|
|
243
|
+
apso open
|
|
1246
244
|
```
|
|
1247
245
|
|
|
1248
|
-
###
|
|
246
|
+
### `apso projects`
|
|
1249
247
|
|
|
1250
|
-
|
|
248
|
+
List services in a workspace.
|
|
1251
249
|
|
|
1252
|
-
```
|
|
1253
|
-
|
|
1254
|
-
request.workspaceId = user.currentWorkspaceId;
|
|
1255
|
-
request.user = { roles: ['user'] };
|
|
1256
|
-
```
|
|
1257
|
-
|
|
1258
|
-
### Complete Example
|
|
1259
|
-
|
|
1260
|
-
```json
|
|
1261
|
-
{
|
|
1262
|
-
"version": 2,
|
|
1263
|
-
"entities": [
|
|
1264
|
-
{
|
|
1265
|
-
"name": "Workspace",
|
|
1266
|
-
"fields": [{ "name": "name", "type": "text" }]
|
|
1267
|
-
},
|
|
1268
|
-
{
|
|
1269
|
-
"name": "Project",
|
|
1270
|
-
"scopeBy": "workspaceId",
|
|
1271
|
-
"fields": [{ "name": "name", "type": "text" }]
|
|
1272
|
-
},
|
|
1273
|
-
{
|
|
1274
|
-
"name": "Task",
|
|
1275
|
-
"scopeBy": ["workspaceId", "projectId"],
|
|
1276
|
-
"fields": [{ "name": "title", "type": "text" }]
|
|
1277
|
-
},
|
|
1278
|
-
{
|
|
1279
|
-
"name": "Comment",
|
|
1280
|
-
"scopeBy": "task.workspaceId",
|
|
1281
|
-
"scopeOptions": {
|
|
1282
|
-
"enforceOn": ["find", "get", "create", "delete"]
|
|
1283
|
-
},
|
|
1284
|
-
"fields": [{ "name": "text", "type": "text" }]
|
|
1285
|
-
}
|
|
1286
|
-
],
|
|
1287
|
-
"relationships": [
|
|
1288
|
-
{ "from": "Project", "to": "Workspace", "type": "ManyToOne" },
|
|
1289
|
-
{ "from": "Task", "to": "Project", "type": "ManyToOne" },
|
|
1290
|
-
{ "from": "Comment", "to": "Task", "type": "ManyToOne" }
|
|
1291
|
-
]
|
|
1292
|
-
}
|
|
250
|
+
```bash
|
|
251
|
+
apso projects
|
|
1293
252
|
```
|
|
1294
253
|
|
|
1295
|
-
###
|
|
1296
|
-
|
|
1297
|
-
**Scoping** (what `scopeBy` provides):
|
|
1298
|
-
- Answers: "Which rows can this user see/modify?"
|
|
1299
|
-
- Data isolation based on tenant/workspace membership
|
|
1300
|
-
- Automatic filtering and injection
|
|
1301
|
-
|
|
1302
|
-
**Authorization** (separate concern, not covered by `scopeBy`):
|
|
1303
|
-
- Answers: "Can this user perform this action?"
|
|
1304
|
-
- Role-based access control (RBAC)
|
|
1305
|
-
- Permission checking (create, read, update, delete)
|
|
1306
|
-
|
|
1307
|
-
These are intentionally separate. Use `scopeBy` for data isolation, and implement authorization guards separately for permission checking.
|
|
1308
|
-
|
|
1309
|
-
---
|
|
254
|
+
### `apso config`
|
|
1310
255
|
|
|
1311
|
-
|
|
256
|
+
View or modify CLI configuration.
|
|
1312
257
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
1. **AuthGuard runs first** - Validates the session/token and populates `request.auth`
|
|
1320
|
-
2. **ScopeGuard runs second** - Reads `organizationId`/`workspaceId` from `request.auth` and enforces isolation
|
|
1321
|
-
|
|
1322
|
-
The `AuthContext` automatically provides the scope values that `scopeBy` needs:
|
|
1323
|
-
|
|
1324
|
-
```typescript
|
|
1325
|
-
// AuthGuard sets this on every authenticated request:
|
|
1326
|
-
request.auth = {
|
|
1327
|
-
userId: "user_123",
|
|
1328
|
-
organizationId: "org_456", // <-- ScopeGuard uses this
|
|
1329
|
-
workspaceId: "org_456", // <-- Or this (alias)
|
|
1330
|
-
roles: ["admin"],
|
|
1331
|
-
// ...
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
// ScopeGuard then uses organizationId to:
|
|
1335
|
-
// - Filter GET /projects -> only org_456's projects
|
|
1336
|
-
// - Inject on POST /projects -> auto-set organizationId
|
|
1337
|
-
// - Verify on GET /projects/:id -> ensure it belongs to org_456
|
|
1338
|
-
```
|
|
1339
|
-
|
|
1340
|
-
### Complete Multi-Tenant Example
|
|
1341
|
-
|
|
1342
|
-
```json
|
|
1343
|
-
{
|
|
1344
|
-
"version": 2,
|
|
1345
|
-
"auth": {
|
|
1346
|
-
"provider": "better-auth",
|
|
1347
|
-
"sessionEntity": "session",
|
|
1348
|
-
"userEntity": "User",
|
|
1349
|
-
"accountUserEntity": "AccountUser",
|
|
1350
|
-
"organizationField": "organizationId"
|
|
1351
|
-
},
|
|
1352
|
-
"entities": [
|
|
1353
|
-
{
|
|
1354
|
-
"name": "User",
|
|
1355
|
-
"fields": [
|
|
1356
|
-
{ "name": "email", "type": "text", "unique": true },
|
|
1357
|
-
{ "name": "name", "type": "text", "nullable": true }
|
|
1358
|
-
]
|
|
1359
|
-
},
|
|
1360
|
-
{
|
|
1361
|
-
"name": "session",
|
|
1362
|
-
"fields": [
|
|
1363
|
-
{ "name": "token", "type": "text", "unique": true },
|
|
1364
|
-
{ "name": "expiresAt", "type": "timestamp" },
|
|
1365
|
-
{ "name": "userId", "type": "text" }
|
|
1366
|
-
]
|
|
1367
|
-
},
|
|
1368
|
-
{
|
|
1369
|
-
"name": "Organization",
|
|
1370
|
-
"fields": [
|
|
1371
|
-
{ "name": "name", "type": "text" },
|
|
1372
|
-
{ "name": "plan", "type": "enum", "values": ["free", "pro", "enterprise"] }
|
|
1373
|
-
]
|
|
1374
|
-
},
|
|
1375
|
-
{
|
|
1376
|
-
"name": "AccountUser",
|
|
1377
|
-
"fields": [
|
|
1378
|
-
{ "name": "role", "type": "enum", "values": ["owner", "admin", "member"] }
|
|
1379
|
-
]
|
|
1380
|
-
},
|
|
1381
|
-
{
|
|
1382
|
-
"name": "Project",
|
|
1383
|
-
"scopeBy": "organizationId",
|
|
1384
|
-
"fields": [
|
|
1385
|
-
{ "name": "name", "type": "text" },
|
|
1386
|
-
{ "name": "status", "type": "enum", "values": ["active", "archived"] }
|
|
1387
|
-
]
|
|
1388
|
-
},
|
|
1389
|
-
{
|
|
1390
|
-
"name": "Task",
|
|
1391
|
-
"scopeBy": ["organizationId", "projectId"],
|
|
1392
|
-
"fields": [
|
|
1393
|
-
{ "name": "title", "type": "text" },
|
|
1394
|
-
{ "name": "completed", "type": "boolean", "default": false }
|
|
1395
|
-
]
|
|
1396
|
-
},
|
|
1397
|
-
{
|
|
1398
|
-
"name": "AuditLog",
|
|
1399
|
-
"scopeBy": "organizationId",
|
|
1400
|
-
"scopeOptions": {
|
|
1401
|
-
"injectOnCreate": true,
|
|
1402
|
-
"enforceOn": ["find", "get"],
|
|
1403
|
-
"bypassRoles": ["superadmin"]
|
|
1404
|
-
},
|
|
1405
|
-
"fields": [
|
|
1406
|
-
{ "name": "action", "type": "text" },
|
|
1407
|
-
{ "name": "details", "type": "json" }
|
|
1408
|
-
]
|
|
1409
|
-
}
|
|
1410
|
-
],
|
|
1411
|
-
"relationships": [
|
|
1412
|
-
{ "from": "AccountUser", "to": "User", "type": "ManyToOne" },
|
|
1413
|
-
{ "from": "AccountUser", "to": "Organization", "type": "ManyToOne" },
|
|
1414
|
-
{ "from": "Project", "to": "Organization", "type": "ManyToOne" },
|
|
1415
|
-
{ "from": "Task", "to": "Project", "type": "ManyToOne" },
|
|
1416
|
-
{ "from": "Task", "to": "Organization", "type": "ManyToOne" },
|
|
1417
|
-
{ "from": "AuditLog", "to": "Organization", "type": "ManyToOne" }
|
|
1418
|
-
]
|
|
1419
|
-
}
|
|
1420
|
-
```
|
|
1421
|
-
|
|
1422
|
-
### Guard Execution Order
|
|
1423
|
-
|
|
1424
|
-
Enable both guards globally for automatic protection:
|
|
1425
|
-
|
|
1426
|
-
```typescript
|
|
1427
|
-
// src/guards/guards.module.ts
|
|
1428
|
-
providers: [
|
|
1429
|
-
AuthGuard,
|
|
1430
|
-
ScopeGuard,
|
|
1431
|
-
{
|
|
1432
|
-
provide: APP_GUARD,
|
|
1433
|
-
useClass: AuthGuard, // Runs first
|
|
1434
|
-
},
|
|
1435
|
-
{
|
|
1436
|
-
provide: APP_GUARD,
|
|
1437
|
-
useClass: ScopeGuard, // Runs second
|
|
1438
|
-
},
|
|
1439
|
-
],
|
|
258
|
+
```bash
|
|
259
|
+
apso config # Show all settings
|
|
260
|
+
apso config get apiUrl # Get a specific value
|
|
261
|
+
apso config set verbose true # Set a value
|
|
262
|
+
apso config reset # Reset to defaults
|
|
1440
263
|
```
|
|
1441
264
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
| Layer | Guard | Question Answered | Configuration |
|
|
1445
|
-
|-------|-------|-------------------|---------------|
|
|
1446
|
-
| 1. Identity | AuthGuard | "Who is this user?" | `auth` in `.apsorc` |
|
|
1447
|
-
| 2. Isolation | ScopeGuard | "Which data can they see?" | `scopeBy` on entities |
|
|
1448
|
-
| 3. Authorization | (Your implementation) | "What actions can they take?" | Custom RBAC guard |
|
|
1449
|
-
|
|
1450
|
-
Apso handles layers 1 and 2 automatically. Layer 3 (fine-grained permissions like "can edit this specific resource") is left to your business logic since it varies widely between applications.
|
|
265
|
+
**Configuration keys:**
|
|
1451
266
|
|
|
1452
|
-
|
|
267
|
+
| Key | Type | Description |
|
|
268
|
+
|-----|------|-------------|
|
|
269
|
+
| `apiUrl` | string | Platform API URL |
|
|
270
|
+
| `webUrl` | string | Platform web URL |
|
|
271
|
+
| `verbose` | boolean | Enable verbose output |
|
|
272
|
+
| `noColor` | boolean | Disable colored output |
|
|
273
|
+
| `telemetryDisabled` | boolean | Opt out of anonymous telemetry |
|
|
274
|
+
| `defaultWorkspace` | string | Default workspace slug |
|
|
1453
275
|
|
|
1454
|
-
|
|
1455
|
-
|---------|----------|------|
|
|
1456
|
-
| **Auth** | Built-in, proprietary | Bring your own, code you own |
|
|
1457
|
-
| **Data Isolation** | PostgreSQL RLS (opaque) | Application-layer guards (transparent) |
|
|
1458
|
-
| **Portability** | Locked to Supabase | Works with any database |
|
|
1459
|
-
| **Debugging** | Database logs, hard to trace | Standard NestJS code, full visibility |
|
|
1460
|
-
| **Customization** | Limited to RLS policies | Unlimited - it's your code |
|
|
1461
|
-
| **Migration Path** | Rewrite required | Change config, regenerate |
|
|
276
|
+
Boolean values accept `true`/`false` or `1`/`0`.
|
|
1462
277
|
|
|
1463
|
-
|
|
1464
|
-
- **Full code ownership** - No vendor lock-in
|
|
1465
|
-
- **Provider flexibility** - Auth0, Clerk, Cognito, or self-hosted
|
|
1466
|
-
- **Database freedom** - PostgreSQL, MySQL, MongoDB, or any TypeORM-supported database
|
|
1467
|
-
- **Complete transparency** - Debug with standard tools, not vendor-specific dashboards
|
|
278
|
+
Environment variables override config file values:
|
|
1468
279
|
|
|
1469
|
-
|
|
280
|
+
| Variable | Overrides |
|
|
281
|
+
|----------|-----------|
|
|
282
|
+
| `APSO_API_URL` | `apiUrl` |
|
|
283
|
+
| `APSO_WEB_URL` | `webUrl` |
|
|
284
|
+
| `APSO_DEBUG=true` | `verbose` |
|
|
285
|
+
| `NO_COLOR` or `APSO_NO_COLOR=true` | `noColor` |
|
|
1470
286
|
|
|
1471
|
-
|
|
287
|
+
### `apso schema`
|
|
1472
288
|
|
|
1473
|
-
|
|
289
|
+
Manage schema synchronization between local `.apsorc` and the platform.
|
|
1474
290
|
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
// ... see full contents in apso-cli/apsorc.schema.json ...
|
|
1481
|
-
{
|
|
1482
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
1483
|
-
"title": "APSO Configuration Schema",
|
|
1484
|
-
"description": "Schema for the .apsorc file used by APSO to define entities and relationships.",
|
|
1485
|
-
// ... (truncated for brevity) ...
|
|
1486
|
-
}
|
|
291
|
+
```bash
|
|
292
|
+
apso schema validate # Validate local schema
|
|
293
|
+
apso schema diff # Show diff between local and remote
|
|
294
|
+
apso schema push # Push local schema to platform
|
|
295
|
+
apso schema pull # Pull remote schema to local
|
|
1487
296
|
```
|
|
1488
297
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
## Debugging
|
|
298
|
+
## Global options
|
|
1492
299
|
|
|
1493
|
-
|
|
300
|
+
These options work with any command:
|
|
1494
301
|
|
|
1495
|
-
```
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
Then just add the debug statements wherever you want.
|
|
1500
|
-
|
|
1501
|
-
```sh-session
|
|
1502
|
-
debug(`variable1 value is:`, variable1);
|
|
302
|
+
```bash
|
|
303
|
+
apso [command] --help # Show command help
|
|
304
|
+
apso [command] --version # Show CLI version
|
|
1503
305
|
```
|
|
1504
306
|
|
|
1505
|
-
|
|
307
|
+
## Supported languages
|
|
1506
308
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
309
|
+
| Language | Framework | ORM | Status |
|
|
310
|
+
|----------|-----------|-----|--------|
|
|
311
|
+
| TypeScript | NestJS | TypeORM | Stable |
|
|
312
|
+
| Python | FastAPI | SQLAlchemy | In development |
|
|
313
|
+
| Go | Gin | GORM | In development |
|
|
1510
314
|
|
|
1511
|
-
|
|
315
|
+
## Contribute
|
|
1512
316
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
317
|
+
```bash
|
|
318
|
+
git clone https://github.com/apsoai/cli.git
|
|
319
|
+
cd cli
|
|
320
|
+
npm install
|
|
1516
321
|
npm run build
|
|
1517
322
|
```
|
|
1518
323
|
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
- [`apso help [COMMANDS]`](#apso-help-commands)
|
|
1522
|
-
- [`apso plugins`](#apso-plugins)
|
|
1523
|
-
- [`apso plugins:install PLUGIN...`](#apso-pluginsinstall-plugin)
|
|
1524
|
-
- [`apso plugins:inspect PLUGIN...`](#apso-pluginsinspect-plugin)
|
|
1525
|
-
- [`apso plugins:install PLUGIN...`](#apso-pluginsinstall-plugin-1)
|
|
1526
|
-
- [`apso plugins:link PLUGIN`](#apso-pluginslink-plugin)
|
|
1527
|
-
- [`apso plugins:uninstall PLUGIN...`](#apso-pluginsuninstall-plugin)
|
|
1528
|
-
- [`apso plugins:uninstall PLUGIN...`](#apso-pluginsuninstall-plugin-1)
|
|
1529
|
-
- [`apso plugins:uninstall PLUGIN...`](#apso-pluginsuninstall-plugin-2)
|
|
1530
|
-
- [`apso plugins update`](#apso-plugins-update)
|
|
1531
|
-
|
|
1532
|
-
## `apso help [COMMANDS]`
|
|
1533
|
-
|
|
1534
|
-
Display help for apso.
|
|
1535
|
-
|
|
1536
|
-
```
|
|
1537
|
-
USAGE
|
|
1538
|
-
$ apso help [COMMANDS] [-n]
|
|
1539
|
-
|
|
1540
|
-
ARGUMENTS
|
|
1541
|
-
COMMANDS Command to show help for.
|
|
1542
|
-
|
|
1543
|
-
FLAGS
|
|
1544
|
-
-n, --nested-commands Include all nested commands in the output.
|
|
1545
|
-
|
|
1546
|
-
DESCRIPTION
|
|
1547
|
-
Display help for apso.
|
|
1548
|
-
```
|
|
1549
|
-
|
|
1550
|
-
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.2.9/src/commands/help.ts)_
|
|
1551
|
-
|
|
1552
|
-
## `apso plugins`
|
|
1553
|
-
|
|
1554
|
-
List installed plugins.
|
|
1555
|
-
|
|
1556
|
-
```
|
|
1557
|
-
USAGE
|
|
1558
|
-
$ apso plugins [--json] [--core]
|
|
1559
|
-
|
|
1560
|
-
FLAGS
|
|
1561
|
-
--core Show core plugins.
|
|
1562
|
-
|
|
1563
|
-
GLOBAL FLAGS
|
|
1564
|
-
--json Format output as json.
|
|
1565
|
-
|
|
1566
|
-
DESCRIPTION
|
|
1567
|
-
List installed plugins.
|
|
1568
|
-
|
|
1569
|
-
EXAMPLES
|
|
1570
|
-
$ apso plugins
|
|
1571
|
-
```
|
|
1572
|
-
|
|
1573
|
-
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.1.2/src/commands/plugins/index.ts)_
|
|
1574
|
-
|
|
1575
|
-
## `apso plugins:install PLUGIN...`
|
|
1576
|
-
|
|
1577
|
-
Installs a plugin into the CLI.
|
|
1578
|
-
|
|
1579
|
-
```
|
|
1580
|
-
USAGE
|
|
1581
|
-
$ apso plugins:install PLUGIN...
|
|
1582
|
-
|
|
1583
|
-
ARGUMENTS
|
|
1584
|
-
PLUGIN Plugin to install.
|
|
1585
|
-
|
|
1586
|
-
FLAGS
|
|
1587
|
-
-f, --force Run yarn install with force flag.
|
|
1588
|
-
-h, --help Show CLI help.
|
|
1589
|
-
-v, --verbose
|
|
1590
|
-
|
|
1591
|
-
DESCRIPTION
|
|
1592
|
-
Installs a plugin into the CLI.
|
|
1593
|
-
Can be installed from npm or a git url.
|
|
324
|
+
To run commands from the local build:
|
|
1594
325
|
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
will override the core plugin implementation. This is useful if a user needs to update core plugin functionality in
|
|
1599
|
-
the CLI without the need to patch and update the whole CLI.
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
ALIASES
|
|
1603
|
-
$ apso plugins add
|
|
1604
|
-
|
|
1605
|
-
EXAMPLES
|
|
1606
|
-
$ apso plugins:install myplugin
|
|
1607
|
-
|
|
1608
|
-
$ apso plugins:install https://github.com/someuser/someplugin
|
|
1609
|
-
|
|
1610
|
-
$ apso plugins:install someuser/someplugin
|
|
1611
|
-
```
|
|
1612
|
-
|
|
1613
|
-
## `apso plugins:inspect PLUGIN...`
|
|
1614
|
-
|
|
1615
|
-
Displays installation properties of a plugin.
|
|
1616
|
-
|
|
1617
|
-
```
|
|
1618
|
-
USAGE
|
|
1619
|
-
$ apso plugins:inspect PLUGIN...
|
|
1620
|
-
|
|
1621
|
-
ARGUMENTS
|
|
1622
|
-
PLUGIN [default: .] Plugin to inspect.
|
|
1623
|
-
|
|
1624
|
-
FLAGS
|
|
1625
|
-
-h, --help Show CLI help.
|
|
1626
|
-
-v, --verbose
|
|
1627
|
-
|
|
1628
|
-
GLOBAL FLAGS
|
|
1629
|
-
--json Format output as json.
|
|
1630
|
-
|
|
1631
|
-
DESCRIPTION
|
|
1632
|
-
Displays installation properties of a plugin.
|
|
1633
|
-
|
|
1634
|
-
EXAMPLES
|
|
1635
|
-
$ apso plugins:inspect myplugin
|
|
1636
|
-
```
|
|
1637
|
-
|
|
1638
|
-
## `apso plugins:install PLUGIN...`
|
|
1639
|
-
|
|
1640
|
-
Installs a plugin into the CLI.
|
|
1641
|
-
|
|
1642
|
-
```
|
|
1643
|
-
USAGE
|
|
1644
|
-
$ apso plugins:install PLUGIN...
|
|
1645
|
-
|
|
1646
|
-
ARGUMENTS
|
|
1647
|
-
PLUGIN Plugin to install.
|
|
1648
|
-
|
|
1649
|
-
FLAGS
|
|
1650
|
-
-f, --force Run yarn install with force flag.
|
|
1651
|
-
-h, --help Show CLI help.
|
|
1652
|
-
-v, --verbose
|
|
1653
|
-
|
|
1654
|
-
DESCRIPTION
|
|
1655
|
-
Installs a plugin into the CLI.
|
|
1656
|
-
Can be installed from npm or a git url.
|
|
1657
|
-
|
|
1658
|
-
Installation of a user-installed plugin will override a core plugin.
|
|
1659
|
-
|
|
1660
|
-
e.g. If you have a core plugin that has a 'hello' command, installing a user-installed plugin with a 'hello' command
|
|
1661
|
-
will override the core plugin implementation. This is useful if a user needs to update core plugin functionality in
|
|
1662
|
-
the CLI without the need to patch and update the whole CLI.
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
ALIASES
|
|
1666
|
-
$ apso plugins add
|
|
1667
|
-
|
|
1668
|
-
EXAMPLES
|
|
1669
|
-
$ apso plugins:install myplugin
|
|
1670
|
-
|
|
1671
|
-
$ apso plugins:install https://github.com/someuser/someplugin
|
|
1672
|
-
|
|
1673
|
-
$ apso plugins:install someuser/someplugin
|
|
1674
|
-
```
|
|
1675
|
-
|
|
1676
|
-
## `apso plugins:link PLUGIN`
|
|
1677
|
-
|
|
1678
|
-
Links a plugin into the CLI for development.
|
|
1679
|
-
|
|
1680
|
-
```
|
|
1681
|
-
USAGE
|
|
1682
|
-
$ apso plugins:link PLUGIN
|
|
1683
|
-
|
|
1684
|
-
ARGUMENTS
|
|
1685
|
-
PATH [default: .] path to plugin
|
|
1686
|
-
|
|
1687
|
-
FLAGS
|
|
1688
|
-
-h, --help Show CLI help.
|
|
1689
|
-
-v, --verbose
|
|
1690
|
-
|
|
1691
|
-
DESCRIPTION
|
|
1692
|
-
Links a plugin into the CLI for development.
|
|
1693
|
-
Installation of a linked plugin will override a user-installed or core plugin.
|
|
1694
|
-
|
|
1695
|
-
e.g. If you have a user-installed or core plugin that has a 'hello' command, installing a linked plugin with a 'hello'
|
|
1696
|
-
command will override the user-installed or core plugin implementation. This is useful for development work.
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
EXAMPLES
|
|
1700
|
-
$ apso plugins:link myplugin
|
|
1701
|
-
```
|
|
1702
|
-
|
|
1703
|
-
## `apso plugins:uninstall PLUGIN...`
|
|
1704
|
-
|
|
1705
|
-
Removes a plugin from the CLI.
|
|
1706
|
-
|
|
1707
|
-
```
|
|
1708
|
-
USAGE
|
|
1709
|
-
$ apso plugins:uninstall PLUGIN...
|
|
1710
|
-
|
|
1711
|
-
ARGUMENTS
|
|
1712
|
-
PLUGIN plugin to uninstall
|
|
1713
|
-
|
|
1714
|
-
FLAGS
|
|
1715
|
-
-h, --help Show CLI help.
|
|
1716
|
-
-v, --verbose
|
|
1717
|
-
|
|
1718
|
-
DESCRIPTION
|
|
1719
|
-
Removes a plugin from the CLI.
|
|
1720
|
-
|
|
1721
|
-
ALIASES
|
|
1722
|
-
$ apso plugins unlink
|
|
1723
|
-
$ apso plugins remove
|
|
1724
|
-
```
|
|
1725
|
-
|
|
1726
|
-
## `apso plugins:uninstall PLUGIN...`
|
|
1727
|
-
|
|
1728
|
-
Removes a plugin from the CLI.
|
|
1729
|
-
|
|
1730
|
-
```
|
|
1731
|
-
USAGE
|
|
1732
|
-
$ apso plugins:uninstall PLUGIN...
|
|
1733
|
-
|
|
1734
|
-
ARGUMENTS
|
|
1735
|
-
PLUGIN plugin to uninstall
|
|
1736
|
-
|
|
1737
|
-
FLAGS
|
|
1738
|
-
-h, --help Show CLI help.
|
|
1739
|
-
-v, --verbose
|
|
1740
|
-
|
|
1741
|
-
DESCRIPTION
|
|
1742
|
-
Removes a plugin from the CLI.
|
|
1743
|
-
|
|
1744
|
-
ALIASES
|
|
1745
|
-
$ apso plugins unlink
|
|
1746
|
-
$ apso plugins remove
|
|
326
|
+
```bash
|
|
327
|
+
./bin/run generate
|
|
328
|
+
./bin/run migrate --sql
|
|
1747
329
|
```
|
|
1748
330
|
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
Removes a plugin from the CLI.
|
|
331
|
+
To develop continuously:
|
|
1752
332
|
|
|
333
|
+
```bash
|
|
334
|
+
npm run build # Rebuild after changes
|
|
335
|
+
npm link # Make 'apso' command available globally
|
|
1753
336
|
```
|
|
1754
|
-
USAGE
|
|
1755
|
-
$ apso plugins:uninstall PLUGIN...
|
|
1756
|
-
|
|
1757
|
-
ARGUMENTS
|
|
1758
|
-
PLUGIN plugin to uninstall
|
|
1759
|
-
|
|
1760
|
-
FLAGS
|
|
1761
|
-
-h, --help Show CLI help.
|
|
1762
|
-
-v, --verbose
|
|
1763
337
|
|
|
1764
|
-
|
|
1765
|
-
Removes a plugin from the CLI.
|
|
338
|
+
### Testing
|
|
1766
339
|
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
340
|
+
```bash
|
|
341
|
+
npm run test # Run all tests
|
|
342
|
+
npm run test:watch # Watch mode
|
|
343
|
+
npm run test:cov # Coverage report
|
|
1770
344
|
```
|
|
1771
345
|
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
Update installed plugins.
|
|
346
|
+
### Debugging
|
|
1775
347
|
|
|
348
|
+
```bash
|
|
349
|
+
env DEBUG=* ./bin/run generate
|
|
1776
350
|
```
|
|
1777
|
-
USAGE
|
|
1778
|
-
$ apso plugins update [-h] [-v]
|
|
1779
351
|
|
|
1780
|
-
|
|
1781
|
-
-h, --help Show CLI help.
|
|
1782
|
-
-v, --verbose
|
|
352
|
+
## License
|
|
1783
353
|
|
|
1784
|
-
|
|
1785
|
-
Update installed plugins.
|
|
1786
|
-
```
|
|
354
|
+
MIT
|