@terreno/api 0.0.18 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.local.md +204 -0
- package/.cursor/rules/00-root.mdc +338 -0
- package/.github/copilot-instructions.md +333 -0
- package/AGENTS.md +23 -3
- package/README.md +73 -3
- package/dist/api.d.ts +68 -1
- package/dist/api.js +139 -4
- package/dist/api.test.js +906 -2
- package/dist/auth.js +3 -1
- package/dist/errors.js +14 -11
- package/dist/example.js +7 -7
- package/dist/githubAuth.test.js +3 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/openApi.test.js +8 -5
- package/dist/openApiBuilder.d.ts +69 -1
- package/dist/openApiBuilder.js +109 -5
- package/dist/openApiValidator.d.ts +296 -0
- package/dist/openApiValidator.js +698 -0
- package/dist/openApiValidator.test.d.ts +1 -0
- package/dist/openApiValidator.test.js +346 -0
- package/dist/plugins.test.js +3 -3
- package/dist/terrenoPlugin.d.ts +4 -0
- package/dist/terrenoPlugin.js +2 -0
- package/dist/tests.js +34 -24
- package/package.json +4 -1
- package/src/__snapshots__/openApi.test.ts.snap +399 -0
- package/src/__snapshots__/openApiBuilder.test.ts.snap +108 -0
- package/src/api.test.ts +743 -2
- package/src/api.ts +209 -3
- package/src/auth.ts +3 -1
- package/src/errors.ts +14 -11
- package/src/example.ts +7 -7
- package/src/githubAuth.test.ts +3 -3
- package/src/index.ts +2 -0
- package/src/openApi.test.ts +8 -5
- package/src/openApiBuilder.ts +188 -15
- package/src/openApiValidator.test.ts +241 -0
- package/src/openApiValidator.ts +860 -0
- package/src/plugins.test.ts +3 -3
- package/src/terrenoPlugin.ts +5 -0
- package/src/tests.ts +34 -24
- package/.cursorrules +0 -107
- package/.windsurfrules +0 -107
- package/dist/response.d.ts +0 -0
- package/dist/response.js +0 -1
- package/index.ts +0 -1
- package/src/response.ts +0 -0
package/src/plugins.test.ts
CHANGED
|
@@ -39,9 +39,9 @@ interface StuffModelType extends Model<Stuff> {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const stuffSchema = new Schema<Stuff>({
|
|
42
|
-
date: DateOnly,
|
|
43
|
-
name: String,
|
|
44
|
-
ownerId: String,
|
|
42
|
+
date: {description: "The date associated with this item", type: DateOnly as any},
|
|
43
|
+
name: {description: "The name of the item", type: String},
|
|
44
|
+
ownerId: {description: "The user who owns this item", type: String},
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
stuffSchema.plugin(isDeletedPlugin);
|
package/src/tests.ts
CHANGED
|
@@ -54,10 +54,10 @@ export interface Food {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const userSchema = new Schema<User>({
|
|
57
|
-
admin: {default: false, type: Boolean},
|
|
58
|
-
age: Number,
|
|
59
|
-
name: String,
|
|
60
|
-
username: String,
|
|
57
|
+
admin: {default: false, description: "Whether the user has admin privileges", type: Boolean},
|
|
58
|
+
age: {description: "The user's age", type: Number},
|
|
59
|
+
name: {description: "The user's display name", type: String},
|
|
60
|
+
username: {description: "The user's username", type: String},
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
userSchema.plugin(passportLocalMongoose as any, {
|
|
@@ -80,55 +80,65 @@ userSchema.methods.postCreate = async function (body: any) {
|
|
|
80
80
|
export const UserModel = model<User>("User", userSchema);
|
|
81
81
|
|
|
82
82
|
const superUserSchema = new Schema<SuperUser>({
|
|
83
|
-
superTitle: {required: true, type: String},
|
|
83
|
+
superTitle: {description: "The super user's title", required: true, type: String},
|
|
84
84
|
});
|
|
85
85
|
export const SuperUserModel = UserModel.discriminator("SuperUser", superUserSchema);
|
|
86
86
|
|
|
87
87
|
const staffUserSchema = new Schema<StaffUser>({
|
|
88
|
-
department: {
|
|
88
|
+
department: {
|
|
89
|
+
description: "The department the staff member belongs to",
|
|
90
|
+
required: true,
|
|
91
|
+
type: String,
|
|
92
|
+
},
|
|
89
93
|
});
|
|
90
94
|
export const StaffUserModel = UserModel.discriminator("Staff", staffUserSchema);
|
|
91
95
|
|
|
92
96
|
const foodCategorySchema = new Schema<FoodCategory>(
|
|
93
97
|
{
|
|
94
|
-
name: String,
|
|
95
|
-
show: Boolean,
|
|
98
|
+
name: {description: "The name of the food category", type: String},
|
|
99
|
+
show: {description: "Whether this category is visible", type: Boolean},
|
|
96
100
|
},
|
|
97
101
|
{timestamps: {createdAt: "created", updatedAt: "updated"}}
|
|
98
102
|
);
|
|
99
103
|
|
|
100
104
|
const likesSchema = new Schema<any>({
|
|
101
|
-
likes: Boolean,
|
|
102
|
-
userId: {ref: "User", type: "ObjectId"},
|
|
105
|
+
likes: {description: "Whether the user liked the item", type: Boolean},
|
|
106
|
+
userId: {description: "The user who liked the item", ref: "User", type: "ObjectId"},
|
|
103
107
|
});
|
|
104
108
|
|
|
105
109
|
const foodSchema = new Schema<Food>(
|
|
106
110
|
{
|
|
107
|
-
calories: Number,
|
|
108
|
-
categories: [foodCategorySchema],
|
|
109
|
-
created: Date,
|
|
111
|
+
calories: {description: "Number of calories in the food", type: Number},
|
|
112
|
+
categories: {description: "Categories this food belongs to", type: [foodCategorySchema]},
|
|
113
|
+
created: {description: "When this food was created", type: Date},
|
|
110
114
|
eatenBy: [
|
|
111
115
|
{
|
|
116
|
+
description: "Users who have eaten this food",
|
|
112
117
|
ref: "User",
|
|
113
118
|
required: true,
|
|
114
119
|
type: Schema.Types.ObjectId,
|
|
115
120
|
},
|
|
116
121
|
],
|
|
117
|
-
expiration: DateOnly,
|
|
118
|
-
hidden: {
|
|
122
|
+
expiration: {description: "Expiration date of the food", type: DateOnly as any},
|
|
123
|
+
hidden: {
|
|
124
|
+
default: false,
|
|
125
|
+
description: "Whether this food is hidden from listings",
|
|
126
|
+
type: Boolean,
|
|
127
|
+
},
|
|
119
128
|
lastEatenWith: {
|
|
129
|
+
description: "Map of user names to dates they last ate this food with",
|
|
120
130
|
of: Date,
|
|
121
131
|
type: Map,
|
|
122
132
|
},
|
|
123
|
-
likesIds: {required: true, type: [likesSchema]},
|
|
124
|
-
name: String,
|
|
125
|
-
ownerId: {ref: "User", type: "ObjectId"},
|
|
133
|
+
likesIds: {description: "User likes for this food", required: true, type: [likesSchema]},
|
|
134
|
+
name: {description: "The name of the food", type: String},
|
|
135
|
+
ownerId: {description: "The user who owns this food entry", ref: "User", type: "ObjectId"},
|
|
126
136
|
source: {
|
|
127
|
-
dateAdded: String,
|
|
128
|
-
href: String,
|
|
129
|
-
name: String,
|
|
137
|
+
dateAdded: {description: "When the source was added", type: String},
|
|
138
|
+
href: {description: "URL of the source", type: String},
|
|
139
|
+
name: {description: "Name of the source", type: String},
|
|
130
140
|
},
|
|
131
|
-
tags: [String],
|
|
141
|
+
tags: {description: "Tags associated with this food", type: [String]},
|
|
132
142
|
},
|
|
133
143
|
{strict: "throw", toJSON: {virtuals: true}, toObject: {virtuals: true}}
|
|
134
144
|
);
|
|
@@ -145,8 +155,8 @@ interface RequiredField {
|
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
const requiredSchema = new Schema<RequiredField>({
|
|
148
|
-
about: String,
|
|
149
|
-
name: {required: true, type: String},
|
|
158
|
+
about: {description: "Information about the item", type: String},
|
|
159
|
+
name: {description: "The name of the item", required: true, type: String},
|
|
150
160
|
});
|
|
151
161
|
export const RequiredModel = model<RequiredField>("Required", requiredSchema);
|
|
152
162
|
|
package/.cursorrules
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# @terreno/api
|
|
2
|
-
|
|
3
|
-
REST API framework built on Express/Mongoose, styled after Django REST Framework.
|
|
4
|
-
|
|
5
|
-
## Commands
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
bun run compile # Compile TypeScript
|
|
9
|
-
bun run dev # Watch mode
|
|
10
|
-
bun run test # Run tests
|
|
11
|
-
bun run lint # Lint code
|
|
12
|
-
bun run lint:fix # Fix lint issues
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Architecture
|
|
16
|
-
|
|
17
|
-
### modelRouter
|
|
18
|
-
|
|
19
|
-
Automatically creates RESTful CRUD APIs for Mongoose models with built-in permissions, population, filtering, and lifecycle hooks.
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
import {modelRouter, modelRouterOptions, Permissions} from "@terreno/api";
|
|
23
|
-
|
|
24
|
-
const router = modelRouter(YourModel, {
|
|
25
|
-
permissions: {
|
|
26
|
-
list: [Permissions.IsAuthenticated],
|
|
27
|
-
create: [Permissions.IsAuthenticated],
|
|
28
|
-
read: [Permissions.IsOwner],
|
|
29
|
-
update: [Permissions.IsOwner],
|
|
30
|
-
delete: [], // Disabled
|
|
31
|
-
},
|
|
32
|
-
sort: "-created",
|
|
33
|
-
queryFields: ["_id", "type", "name"],
|
|
34
|
-
});
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### Custom Routes
|
|
38
|
-
|
|
39
|
-
For non-CRUD endpoints, use the OpenAPI builder:
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
import {asyncHandler, authenticateMiddleware, createOpenApiBuilder} from "@terreno/api";
|
|
43
|
-
|
|
44
|
-
router.get("/yourRoute/:id", [
|
|
45
|
-
authenticateMiddleware(),
|
|
46
|
-
createOpenApiBuilder(options)
|
|
47
|
-
.withTags(["yourTag"])
|
|
48
|
-
.withSummary("Brief summary")
|
|
49
|
-
.withPathParameter("id", {type: "string"})
|
|
50
|
-
.withResponse(200, {data: {type: "object"}})
|
|
51
|
-
.build(),
|
|
52
|
-
], asyncHandler(async (req, res) => {
|
|
53
|
-
return res.json({data: result});
|
|
54
|
-
}));
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## Conventions
|
|
58
|
-
|
|
59
|
-
### Error Handling
|
|
60
|
-
- Throw `APIError` with appropriate status codes: `throw new APIError({status: 400, title: "Message"})`
|
|
61
|
-
- Services should throw user-friendly errors
|
|
62
|
-
|
|
63
|
-
### Mongoose
|
|
64
|
-
- Do not use `Model.findOne` - use `Model.findExactlyOne` or `Model.findOneOrThrow`
|
|
65
|
-
- Define statics/methods by direct assignment: `schema.methods = {bar() {}}`
|
|
66
|
-
- All model types live in `src/modelInterfaces.ts`
|
|
67
|
-
|
|
68
|
-
### User Type Casting
|
|
69
|
-
- In API routes: `req.user` is `UserDocument | undefined`
|
|
70
|
-
- In @terreno/api callbacks: cast with `const user = u as unknown as UserDocument`
|
|
71
|
-
- Never use `as any as UserDocument`
|
|
72
|
-
|
|
73
|
-
### Logging
|
|
74
|
-
- Use `logger.info/warn/error/debug` for permanent logs (not `console.log`)
|
|
75
|
-
|
|
76
|
-
### Testing
|
|
77
|
-
- Use bun test with expect for testing
|
|
78
|
-
- Use existing manual mocks from `src/__mocks__/`
|
|
79
|
-
- Never mock @terreno/api or models
|
|
80
|
-
|
|
81
|
-
## Model Type Generation
|
|
82
|
-
|
|
83
|
-
When creating/modifying Mongoose models, update `src/modelInterfaces.ts`:
|
|
84
|
-
|
|
85
|
-
```typescript
|
|
86
|
-
export type YourModelMethods = {
|
|
87
|
-
customMethod: (this: YourModelDocument, param: string) => Promise<void>;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
export type YourModelStatics = DefaultStatics<YourModelDocument> & {
|
|
91
|
-
customStatic: (this: YourModelModel, param: string) => Promise<YourModelDocument>;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
export type YourModelModel = DefaultModel<YourModelDocument> & YourModelStatics;
|
|
95
|
-
export type YourModelSchema = mongoose.Schema<YourModelDocument, YourModelModel, YourModelMethods>;
|
|
96
|
-
export type YourModelDocument = DefaultDoc & YourModelMethods & {
|
|
97
|
-
fieldName: string;
|
|
98
|
-
};
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## SDK Generation
|
|
102
|
-
|
|
103
|
-
After modifying routes, regenerate the SDK:
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
bun run sdk
|
|
107
|
-
```
|
package/.windsurfrules
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# @terreno/api
|
|
2
|
-
|
|
3
|
-
REST API framework built on Express/Mongoose, styled after Django REST Framework.
|
|
4
|
-
|
|
5
|
-
## Commands
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
bun run compile # Compile TypeScript
|
|
9
|
-
bun run dev # Watch mode
|
|
10
|
-
bun run test # Run tests
|
|
11
|
-
bun run lint # Lint code
|
|
12
|
-
bun run lint:fix # Fix lint issues
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Architecture
|
|
16
|
-
|
|
17
|
-
### modelRouter
|
|
18
|
-
|
|
19
|
-
Automatically creates RESTful CRUD APIs for Mongoose models with built-in permissions, population, filtering, and lifecycle hooks.
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
import {modelRouter, modelRouterOptions, Permissions} from "@terreno/api";
|
|
23
|
-
|
|
24
|
-
const router = modelRouter(YourModel, {
|
|
25
|
-
permissions: {
|
|
26
|
-
list: [Permissions.IsAuthenticated],
|
|
27
|
-
create: [Permissions.IsAuthenticated],
|
|
28
|
-
read: [Permissions.IsOwner],
|
|
29
|
-
update: [Permissions.IsOwner],
|
|
30
|
-
delete: [], // Disabled
|
|
31
|
-
},
|
|
32
|
-
sort: "-created",
|
|
33
|
-
queryFields: ["_id", "type", "name"],
|
|
34
|
-
});
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### Custom Routes
|
|
38
|
-
|
|
39
|
-
For non-CRUD endpoints, use the OpenAPI builder:
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
import {asyncHandler, authenticateMiddleware, createOpenApiBuilder} from "@terreno/api";
|
|
43
|
-
|
|
44
|
-
router.get("/yourRoute/:id", [
|
|
45
|
-
authenticateMiddleware(),
|
|
46
|
-
createOpenApiBuilder(options)
|
|
47
|
-
.withTags(["yourTag"])
|
|
48
|
-
.withSummary("Brief summary")
|
|
49
|
-
.withPathParameter("id", {type: "string"})
|
|
50
|
-
.withResponse(200, {data: {type: "object"}})
|
|
51
|
-
.build(),
|
|
52
|
-
], asyncHandler(async (req, res) => {
|
|
53
|
-
return res.json({data: result});
|
|
54
|
-
}));
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## Conventions
|
|
58
|
-
|
|
59
|
-
### Error Handling
|
|
60
|
-
- Throw `APIError` with appropriate status codes: `throw new APIError({status: 400, title: "Message"})`
|
|
61
|
-
- Services should throw user-friendly errors
|
|
62
|
-
|
|
63
|
-
### Mongoose
|
|
64
|
-
- Do not use `Model.findOne` - use `Model.findExactlyOne` or `Model.findOneOrThrow`
|
|
65
|
-
- Define statics/methods by direct assignment: `schema.methods = {bar() {}}`
|
|
66
|
-
- All model types live in `src/modelInterfaces.ts`
|
|
67
|
-
|
|
68
|
-
### User Type Casting
|
|
69
|
-
- In API routes: `req.user` is `UserDocument | undefined`
|
|
70
|
-
- In @terreno/api callbacks: cast with `const user = u as unknown as UserDocument`
|
|
71
|
-
- Never use `as any as UserDocument`
|
|
72
|
-
|
|
73
|
-
### Logging
|
|
74
|
-
- Use `logger.info/warn/error/debug` for permanent logs (not `console.log`)
|
|
75
|
-
|
|
76
|
-
### Testing
|
|
77
|
-
- Use bun test with expect for testing
|
|
78
|
-
- Use existing manual mocks from `src/__mocks__/`
|
|
79
|
-
- Never mock @terreno/api or models
|
|
80
|
-
|
|
81
|
-
## Model Type Generation
|
|
82
|
-
|
|
83
|
-
When creating/modifying Mongoose models, update `src/modelInterfaces.ts`:
|
|
84
|
-
|
|
85
|
-
```typescript
|
|
86
|
-
export type YourModelMethods = {
|
|
87
|
-
customMethod: (this: YourModelDocument, param: string) => Promise<void>;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
export type YourModelStatics = DefaultStatics<YourModelDocument> & {
|
|
91
|
-
customStatic: (this: YourModelModel, param: string) => Promise<YourModelDocument>;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
export type YourModelModel = DefaultModel<YourModelDocument> & YourModelStatics;
|
|
95
|
-
export type YourModelSchema = mongoose.Schema<YourModelDocument, YourModelModel, YourModelMethods>;
|
|
96
|
-
export type YourModelDocument = DefaultDoc & YourModelMethods & {
|
|
97
|
-
fieldName: string;
|
|
98
|
-
};
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## SDK Generation
|
|
102
|
-
|
|
103
|
-
After modifying routes, regenerate the SDK:
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
bun run sdk
|
|
107
|
-
```
|
package/dist/response.d.ts
DELETED
|
File without changes
|
package/dist/response.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";
|
package/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
console.log("Hello via Bun!");
|
package/src/response.ts
DELETED
|
File without changes
|