@common-grants/core 0.1.1 → 0.2.0-alpha.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/lib/api.tsp +79 -0
- package/lib/core/fields/custom-field.tsp +7 -0
- package/lib/core/fields/file.tsp +49 -0
- package/lib/core/fields/index.tsp +1 -0
- package/lib/core/models/application.tsp +30 -80
- package/lib/core/models/competition.tsp +148 -0
- package/lib/core/models/form-response.tsp +71 -0
- package/lib/core/models/form.tsp +119 -0
- package/lib/core/models/index.tsp +5 -0
- package/lib/core/models/mapping.tsp +162 -0
- package/lib/core/models/proposal.tsp +173 -0
- package/lib/core/responses/success.tsp +16 -0
- package/lib/core/routes/applications.tsp +62 -0
- package/lib/core/routes/competitions.tsp +46 -0
- package/lib/core/routes/form-responses.tsp +52 -0
- package/lib/core/routes/index.tsp +3 -0
- package/lib/main.tsp +12 -0
- package/package.json +3 -2
package/lib/api.tsp
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import "./core/index.tsp";
|
|
3
3
|
import "@typespec/http";
|
|
4
4
|
import "@typespec/openapi";
|
|
5
|
+
import "@typespec/versioning";
|
|
5
6
|
|
|
6
7
|
using TypeSpec.Http;
|
|
7
8
|
using TypeSpec.OpenAPI;
|
|
9
|
+
using Versioning;
|
|
8
10
|
|
|
9
11
|
/** The base OpenAPI specification for a CommonGrants API
|
|
10
12
|
*
|
|
@@ -12,6 +14,12 @@ using TypeSpec.OpenAPI;
|
|
|
12
14
|
* it must implement all of the routes with the "required" tag in this specification.
|
|
13
15
|
*/
|
|
14
16
|
@service(#{ title: "CommonGrants Base API" })
|
|
17
|
+
@tagMetadata(
|
|
18
|
+
"experimental",
|
|
19
|
+
#{
|
|
20
|
+
description: "Endpoints that MAY be implemented by CommonGrants APIs, but are not guaranteed to be stable",
|
|
21
|
+
}
|
|
22
|
+
)
|
|
15
23
|
@tagMetadata(
|
|
16
24
|
"optional",
|
|
17
25
|
#{ description: "Endpoints that MAY be implemented by CommonGrants APIs" }
|
|
@@ -22,12 +30,22 @@ using TypeSpec.OpenAPI;
|
|
|
22
30
|
description: "Endpoints that MUST be implemented by all CommonGrants APIs",
|
|
23
31
|
}
|
|
24
32
|
)
|
|
33
|
+
@tagMetadata(
|
|
34
|
+
"Applications",
|
|
35
|
+
#{
|
|
36
|
+
description: "Endpoints related to applications for funding opportunities",
|
|
37
|
+
}
|
|
38
|
+
)
|
|
25
39
|
@tagMetadata(
|
|
26
40
|
"Opportunities",
|
|
27
41
|
#{ description: "Endpoints related to funding opportunities" }
|
|
28
42
|
)
|
|
29
43
|
namespace CommonGrants.API;
|
|
30
44
|
|
|
45
|
+
// #########################################################
|
|
46
|
+
// Opportunities
|
|
47
|
+
// #########################################################
|
|
48
|
+
|
|
31
49
|
@tag("Opportunities")
|
|
32
50
|
@route("/common-grants/opportunities")
|
|
33
51
|
namespace Opportunities {
|
|
@@ -42,3 +60,64 @@ namespace Opportunities {
|
|
|
42
60
|
@tag("optional")
|
|
43
61
|
op search is Router.search;
|
|
44
62
|
}
|
|
63
|
+
|
|
64
|
+
// #########################################################
|
|
65
|
+
// Applications
|
|
66
|
+
// #########################################################
|
|
67
|
+
|
|
68
|
+
@tag("Applications")
|
|
69
|
+
@tag("experimental")
|
|
70
|
+
@route("/common-grants/")
|
|
71
|
+
namespace Apply {
|
|
72
|
+
// #########################################################
|
|
73
|
+
// Direct apply workflow
|
|
74
|
+
// #########################################################
|
|
75
|
+
@tag("experimental")
|
|
76
|
+
@route("/competitions")
|
|
77
|
+
namespace DirectApplyWorkflow {
|
|
78
|
+
alias Router = Routes.Competitions;
|
|
79
|
+
|
|
80
|
+
@added(Versions.v0_2)
|
|
81
|
+
op competitionDetails is Router.read;
|
|
82
|
+
|
|
83
|
+
@added(Versions.v0_2)
|
|
84
|
+
op apply is Router.apply;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// #########################################################
|
|
88
|
+
// Multi-step workflow
|
|
89
|
+
// #########################################################
|
|
90
|
+
|
|
91
|
+
@route("/applications")
|
|
92
|
+
namespace MultiStepWorkflow {
|
|
93
|
+
alias ApplicationRouter = Routes.Applications;
|
|
94
|
+
alias FormResponseRouter = Routes.FormResponses;
|
|
95
|
+
|
|
96
|
+
// ################################
|
|
97
|
+
// Start an application workflow
|
|
98
|
+
// ################################
|
|
99
|
+
|
|
100
|
+
@added(Versions.v0_2)
|
|
101
|
+
op startApplication is ApplicationRouter.startApplication;
|
|
102
|
+
|
|
103
|
+
@added(Versions.v0_2)
|
|
104
|
+
op getApplication is ApplicationRouter.getApplication;
|
|
105
|
+
|
|
106
|
+
// ################################
|
|
107
|
+
// Update form responses
|
|
108
|
+
// ################################
|
|
109
|
+
|
|
110
|
+
@added(Versions.v0_2)
|
|
111
|
+
op setFormResponse is FormResponseRouter.setFormResponse;
|
|
112
|
+
|
|
113
|
+
@added(Versions.v0_2)
|
|
114
|
+
op getFormResponse is FormResponseRouter.getFormResponse;
|
|
115
|
+
|
|
116
|
+
// ################################
|
|
117
|
+
// Submit an application after completing all forms
|
|
118
|
+
// ################################
|
|
119
|
+
|
|
120
|
+
@added(Versions.v0_2)
|
|
121
|
+
op submitApplication is ApplicationRouter.submitApplication;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -76,6 +76,13 @@ namespace Examples.CustomField {
|
|
|
76
76
|
schema: "https://example.com/program-areas.json",
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
+
const agency = #{
|
|
80
|
+
name: "agency",
|
|
81
|
+
fieldType: CustomFieldType.string,
|
|
82
|
+
value: "Department of Transportation",
|
|
83
|
+
description: "The agency responsible for managing this opportunity",
|
|
84
|
+
};
|
|
85
|
+
|
|
79
86
|
/** An example of an array custom field */
|
|
80
87
|
const eligibilityTypes = #{
|
|
81
88
|
name: "eligibilityType",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
namespace CommonGrants.Fields;
|
|
2
|
+
|
|
3
|
+
/** A field representing a downloadable file. */
|
|
4
|
+
@example(Examples.File.imageFile, #{ title: "An image file" })
|
|
5
|
+
@example(Examples.File.pdfFile, #{ title: "A PDF file" })
|
|
6
|
+
model File {
|
|
7
|
+
/** The file's download URL. */
|
|
8
|
+
downloadUrl: url;
|
|
9
|
+
|
|
10
|
+
/** The file's name. */
|
|
11
|
+
name: string;
|
|
12
|
+
|
|
13
|
+
/** The file's description. */
|
|
14
|
+
description?: string;
|
|
15
|
+
|
|
16
|
+
/** The file's size in bytes. */
|
|
17
|
+
sizeInBytes?: numeric;
|
|
18
|
+
|
|
19
|
+
/** The file's MIME type. */
|
|
20
|
+
mimeType?: string;
|
|
21
|
+
|
|
22
|
+
/** The system metadata for the file. */
|
|
23
|
+
...Fields.SystemMetadata;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// #########################################################
|
|
27
|
+
// Examples
|
|
28
|
+
// #########################################################
|
|
29
|
+
|
|
30
|
+
namespace Examples.File {
|
|
31
|
+
const pdfFile = #{
|
|
32
|
+
downloadUrl: "https://example.com/file.pdf",
|
|
33
|
+
name: "example.pdf",
|
|
34
|
+
description: "A PDF file with instructions",
|
|
35
|
+
sizeInBytes: 1000,
|
|
36
|
+
mimeType: "application/pdf",
|
|
37
|
+
createdAt: utcDateTime.fromISO("2025-01-01T17:01:01"),
|
|
38
|
+
lastModifiedAt: utcDateTime.fromISO("2025-01-02T17:30:00"),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const imageFile = #{
|
|
42
|
+
downloadUrl: "https://example.com/image.png",
|
|
43
|
+
name: "image.png",
|
|
44
|
+
sizeInBytes: 1000,
|
|
45
|
+
mimeType: "image/png",
|
|
46
|
+
createdAt: utcDateTime.fromISO("2025-01-01T17:01:01"),
|
|
47
|
+
lastModifiedAt: utcDateTime.fromISO("2025-01-02T17:30:00"),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -1,33 +1,29 @@
|
|
|
1
|
-
import "../index.tsp";
|
|
2
|
-
|
|
3
1
|
namespace CommonGrants.Models;
|
|
4
2
|
|
|
5
|
-
/** The base model for an application. */
|
|
6
|
-
@example(Examples.Application.exampleApplication)
|
|
7
3
|
model ApplicationBase {
|
|
8
|
-
/** The
|
|
4
|
+
/** The unique identifier for the application */
|
|
9
5
|
id: Types.uuid;
|
|
10
6
|
|
|
11
|
-
/** The application
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/** The application's date of submission. */
|
|
15
|
-
dateSubmitted?: Types.isoDate;
|
|
7
|
+
/** The name of the application */
|
|
8
|
+
name: string;
|
|
16
9
|
|
|
17
|
-
/** The
|
|
18
|
-
|
|
10
|
+
/** The unique identifier for the competition */
|
|
11
|
+
competitionId: Types.uuid;
|
|
19
12
|
|
|
20
|
-
/** The
|
|
21
|
-
|
|
13
|
+
/** The form responses for the application */
|
|
14
|
+
formResponses: Record<AppFormResponse>;
|
|
22
15
|
|
|
23
|
-
/** The
|
|
24
|
-
|
|
16
|
+
/** The status of the application */
|
|
17
|
+
status: AppStatus;
|
|
25
18
|
|
|
26
|
-
/** The
|
|
27
|
-
|
|
19
|
+
/** The date and time the application was submitted */
|
|
20
|
+
submittedAt?: utcDateTime | null;
|
|
28
21
|
|
|
29
|
-
/** The
|
|
22
|
+
/** The custom fields about the application */
|
|
30
23
|
customFields?: Record<Fields.CustomField>;
|
|
24
|
+
|
|
25
|
+
/** The system metadata for the application */
|
|
26
|
+
...Fields.SystemMetadata;
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
// #########################################################
|
|
@@ -53,52 +49,29 @@ model AppStatus {
|
|
|
53
49
|
|
|
54
50
|
/** The default set of values accepted for application status. */
|
|
55
51
|
enum AppStatusOptions {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
rejected,
|
|
59
|
-
custom,
|
|
60
|
-
}
|
|
52
|
+
/** The application is a draft */
|
|
53
|
+
draft,
|
|
61
54
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// #########################################################
|
|
65
|
-
|
|
66
|
-
/** The project for which funding is requested. */
|
|
67
|
-
@example(Examples.Application.exampleProposal)
|
|
68
|
-
model AppProposal {
|
|
69
|
-
/** The title of the proposal and/or the project requesting funding. */
|
|
70
|
-
title: string;
|
|
71
|
-
|
|
72
|
-
/** The description of the proposal and/or the project requesting funding. */
|
|
73
|
-
description: string;
|
|
74
|
-
|
|
75
|
-
/** The amount of money requested. */
|
|
76
|
-
amountRequested?: Fields.Money;
|
|
55
|
+
/** The application has been submitted */
|
|
56
|
+
submitted,
|
|
77
57
|
|
|
78
|
-
/** The
|
|
79
|
-
|
|
58
|
+
/** The application has been accepted */
|
|
59
|
+
accepted,
|
|
80
60
|
|
|
81
|
-
/** The
|
|
82
|
-
|
|
61
|
+
/** The application has been rejected */
|
|
62
|
+
rejected,
|
|
83
63
|
|
|
84
|
-
/** The
|
|
85
|
-
|
|
64
|
+
/** The application has a custom status */
|
|
65
|
+
custom,
|
|
86
66
|
}
|
|
87
67
|
|
|
88
68
|
// #########################################################
|
|
89
|
-
//
|
|
69
|
+
// ApplicationFormResponse
|
|
90
70
|
// #########################################################
|
|
91
71
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
id: Types.uuid;
|
|
96
|
-
|
|
97
|
-
/** The opportunity's name. */
|
|
98
|
-
title?: string;
|
|
99
|
-
|
|
100
|
-
/** The opportunity's custom fields. */
|
|
101
|
-
customFields?: Record<Fields.CustomField>;
|
|
72
|
+
model AppFormResponse extends FormResponseBase {
|
|
73
|
+
/** The unique identifier for the application */
|
|
74
|
+
applicationId: Types.uuid;
|
|
102
75
|
}
|
|
103
76
|
|
|
104
77
|
// #########################################################
|
|
@@ -106,19 +79,9 @@ model AppOpportunity {
|
|
|
106
79
|
// #########################################################
|
|
107
80
|
|
|
108
81
|
namespace Examples.Application {
|
|
109
|
-
const exampleApplication = #{
|
|
110
|
-
id: "083b4567-e89d-42c8-a439-6c1234567890",
|
|
111
|
-
status: submittedStatus,
|
|
112
|
-
dateSubmitted: Types.isoDate.fromISO("2024-01-01"),
|
|
113
|
-
organization: Examples.Organization.exampleOrg,
|
|
114
|
-
pointOfContact: Examples.Person.examplePerson,
|
|
115
|
-
proposal: exampleProposal,
|
|
116
|
-
opportunity: exampleOpportunity,
|
|
117
|
-
};
|
|
118
|
-
|
|
119
82
|
const submittedStatus = #{
|
|
120
83
|
value: AppStatusOptions.submitted,
|
|
121
|
-
description: "
|
|
84
|
+
description: "The application has been submitted.",
|
|
122
85
|
};
|
|
123
86
|
|
|
124
87
|
const customStatus = #{
|
|
@@ -126,17 +89,4 @@ namespace Examples.Application {
|
|
|
126
89
|
customValue: "draft",
|
|
127
90
|
description: "Application is started but not yet submitted.",
|
|
128
91
|
};
|
|
129
|
-
|
|
130
|
-
const exampleProposal = #{
|
|
131
|
-
title: "Example Project",
|
|
132
|
-
description: "Example project to serve community needs.",
|
|
133
|
-
amountRequested: #{ amount: "100000", currency: "USD" },
|
|
134
|
-
periodStartDate: Types.isoDate.fromISO("2024-01-01"),
|
|
135
|
-
periodEndDate: Types.isoDate.fromISO("2024-12-31"),
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const exampleOpportunity = #{
|
|
139
|
-
id: "083b4567-e89d-42c8-a439-6c1234567890",
|
|
140
|
-
title: "Example Opportunity",
|
|
141
|
-
};
|
|
142
92
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
namespace CommonGrants.Models;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The base model for a competition.
|
|
5
|
+
*
|
|
6
|
+
* A competition is an application process for a funding opportunity. It often has a
|
|
7
|
+
* distinct application period and set of application forms.
|
|
8
|
+
*/
|
|
9
|
+
@example(Examples.Competition.competition)
|
|
10
|
+
model CompetitionBase {
|
|
11
|
+
/** Globally unique id for the competition */
|
|
12
|
+
id: Types.uuid;
|
|
13
|
+
|
|
14
|
+
/** The opportunity id for the competition */
|
|
15
|
+
opportunityId: Types.uuid;
|
|
16
|
+
|
|
17
|
+
/** The title of the competition */
|
|
18
|
+
title: string;
|
|
19
|
+
|
|
20
|
+
/** The description of the competition */
|
|
21
|
+
description?: string;
|
|
22
|
+
|
|
23
|
+
/** The instructions for the competition */
|
|
24
|
+
instructions?: string | Fields.File;
|
|
25
|
+
|
|
26
|
+
/** The status of the competition */
|
|
27
|
+
status: CompetitionStatus;
|
|
28
|
+
|
|
29
|
+
/** The key dates in the competition timeline */
|
|
30
|
+
keyDates: CompetitionTimeline;
|
|
31
|
+
|
|
32
|
+
/** The forms for the competition */
|
|
33
|
+
forms: CompetitionForms;
|
|
34
|
+
|
|
35
|
+
/** The custom fields for the competition */
|
|
36
|
+
customFields?: Record<Fields.CustomField>;
|
|
37
|
+
|
|
38
|
+
/** The system metadata for the competition */
|
|
39
|
+
...Fields.SystemMetadata;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// #########################################################
|
|
43
|
+
// CompetitionStatus
|
|
44
|
+
// #########################################################
|
|
45
|
+
|
|
46
|
+
/** The status of the competition */
|
|
47
|
+
@example(Examples.Competition.status)
|
|
48
|
+
model CompetitionStatus {
|
|
49
|
+
value: CompetitionStatusOptions;
|
|
50
|
+
customValue?: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// #########################################################
|
|
55
|
+
// CompetitionStatusOptions
|
|
56
|
+
// #########################################################
|
|
57
|
+
|
|
58
|
+
enum CompetitionStatusOptions {
|
|
59
|
+
/** The competition is open for applications */
|
|
60
|
+
open,
|
|
61
|
+
|
|
62
|
+
/** The competition is closed for applications */
|
|
63
|
+
closed,
|
|
64
|
+
|
|
65
|
+
/** The competition is in a custom status */
|
|
66
|
+
custom,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// #########################################################
|
|
70
|
+
// CompetitionForm
|
|
71
|
+
// #########################################################
|
|
72
|
+
|
|
73
|
+
/** Set of forms that need to be completed to apply to the competition. */
|
|
74
|
+
@example(Examples.Competition.forms)
|
|
75
|
+
model CompetitionForms {
|
|
76
|
+
/** The forms for the competition */
|
|
77
|
+
forms: Record<Models.Form>;
|
|
78
|
+
|
|
79
|
+
/** The validation rules for the competition forms */
|
|
80
|
+
validation: Record<unknown>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// #########################################################
|
|
84
|
+
// CompetitionTimeline
|
|
85
|
+
// #########################################################
|
|
86
|
+
|
|
87
|
+
@example(Examples.Competition.keyDates)
|
|
88
|
+
model CompetitionTimeline {
|
|
89
|
+
/** The start date of the competition */
|
|
90
|
+
openDate: Fields.Event;
|
|
91
|
+
|
|
92
|
+
/** The end date of the competition */
|
|
93
|
+
closeDate: Fields.Event;
|
|
94
|
+
|
|
95
|
+
/** The date the competition was created */
|
|
96
|
+
otherDates?: Record<Fields.Event>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// #########################################################
|
|
100
|
+
// Examples
|
|
101
|
+
// #########################################################
|
|
102
|
+
|
|
103
|
+
namespace Examples.Competition {
|
|
104
|
+
const competition = #{
|
|
105
|
+
id: "b7c1e2f4-8a3d-4e2a-9c5b-1f2e3d4c5b6a",
|
|
106
|
+
opportunityId: "b7c1e2f4-8a3d-4e2a-9c5b-1f2e3d4c5b6b",
|
|
107
|
+
title: "Competition 1",
|
|
108
|
+
description: "Competition 1 description",
|
|
109
|
+
instructions: "Competition 1 instructions",
|
|
110
|
+
status: status,
|
|
111
|
+
keyDates: keyDates,
|
|
112
|
+
forms: forms,
|
|
113
|
+
createdAt: utcDateTime.fromISO("2025-01-01T00:00:00Z"),
|
|
114
|
+
lastModifiedAt: utcDateTime.fromISO("2025-01-01T00:00:00Z"),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const keyDates = #{
|
|
118
|
+
openDate: #{
|
|
119
|
+
name: "Open Date",
|
|
120
|
+
eventType: Fields.EventType.singleDate,
|
|
121
|
+
date: Types.isoDate.fromISO("2025-01-01"),
|
|
122
|
+
},
|
|
123
|
+
closeDate: #{
|
|
124
|
+
name: "Close Date",
|
|
125
|
+
eventType: Fields.EventType.singleDate,
|
|
126
|
+
date: Types.isoDate.fromISO("2025-01-30"),
|
|
127
|
+
},
|
|
128
|
+
otherDates: #{
|
|
129
|
+
reviewPeriod: #{
|
|
130
|
+
name: "Application Review Period",
|
|
131
|
+
eventType: Fields.EventType.dateRange,
|
|
132
|
+
startDate: Types.isoDate.fromISO("2025-02-01"),
|
|
133
|
+
endDate: Types.isoDate.fromISO("2025-02-28"),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const status = #{
|
|
139
|
+
value: CompetitionStatusOptions.open,
|
|
140
|
+
customValue: "custom",
|
|
141
|
+
description: "Competition is open for applications",
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const forms = #{
|
|
145
|
+
forms: #{ formA: Examples.Form.form, formB: Examples.Form.form },
|
|
146
|
+
validation: #{ required: #["formA", "formB"] },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
namespace CommonGrants.Models;
|
|
2
|
+
|
|
3
|
+
/** The base model for a form response */
|
|
4
|
+
model FormResponseBase {
|
|
5
|
+
/** The unique identifier for the form response */
|
|
6
|
+
id: Types.uuid;
|
|
7
|
+
|
|
8
|
+
/** The form being responded to */
|
|
9
|
+
form: Form;
|
|
10
|
+
|
|
11
|
+
/** The response to the form */
|
|
12
|
+
response: Record<unknown>;
|
|
13
|
+
|
|
14
|
+
/** The status of the form response */
|
|
15
|
+
status: FormResponseStatus;
|
|
16
|
+
|
|
17
|
+
/** The validation errors for the form response */
|
|
18
|
+
validationErrors: Array<unknown>;
|
|
19
|
+
|
|
20
|
+
/** The system metadata for the form response */
|
|
21
|
+
...Fields.SystemMetadata;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// #########################################################
|
|
25
|
+
// FormResponseStatus
|
|
26
|
+
// #########################################################
|
|
27
|
+
|
|
28
|
+
/** The status of the form response */
|
|
29
|
+
@example(Examples.FormResponse.inProgressStatus)
|
|
30
|
+
model FormResponseStatus {
|
|
31
|
+
/** The status of the form response */
|
|
32
|
+
value: FormResponseStatusOptions;
|
|
33
|
+
|
|
34
|
+
/** A custom value for the status */
|
|
35
|
+
customValue?: string;
|
|
36
|
+
|
|
37
|
+
/** A description of the status */
|
|
38
|
+
description?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// #########################################################
|
|
42
|
+
// FormResponseStatusOptions
|
|
43
|
+
// #########################################################
|
|
44
|
+
|
|
45
|
+
/** The options for the status of the form response */
|
|
46
|
+
enum FormResponseStatusOptions {
|
|
47
|
+
/** The form response has not been started */
|
|
48
|
+
notStarted,
|
|
49
|
+
|
|
50
|
+
/** The form response is in progress */
|
|
51
|
+
inProgress,
|
|
52
|
+
|
|
53
|
+
/** The form response is submitted */
|
|
54
|
+
complete,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// #########################################################
|
|
58
|
+
// Examples
|
|
59
|
+
// #########################################################
|
|
60
|
+
|
|
61
|
+
namespace Examples.FormResponse {
|
|
62
|
+
const inProgressStatus = #{
|
|
63
|
+
value: FormResponseStatusOptions.inProgress,
|
|
64
|
+
description: "The form response is in progress",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const completeStatus = #{
|
|
68
|
+
value: FormResponseStatusOptions.complete,
|
|
69
|
+
description: "The form response is complete",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import "../index.tsp";
|
|
2
|
+
|
|
3
|
+
namespace CommonGrants.Models;
|
|
4
|
+
|
|
5
|
+
// #########################################################
|
|
6
|
+
// Form
|
|
7
|
+
// #########################################################
|
|
8
|
+
|
|
9
|
+
/** A form for collecting data from a user. */
|
|
10
|
+
@example(Examples.Form.form)
|
|
11
|
+
model Form {
|
|
12
|
+
/** The form's unique identifier. */
|
|
13
|
+
id: Types.uuid;
|
|
14
|
+
|
|
15
|
+
/** The form's name. */
|
|
16
|
+
name: string;
|
|
17
|
+
|
|
18
|
+
/** The form's description. */
|
|
19
|
+
description?: string;
|
|
20
|
+
|
|
21
|
+
/** The form's instructions. */
|
|
22
|
+
instructions?: string | Fields.File;
|
|
23
|
+
|
|
24
|
+
/** The form's JSON schema used to render the form and validate responses. */
|
|
25
|
+
jsonSchema?: FormJsonSchema;
|
|
26
|
+
|
|
27
|
+
/** The form's UI schema used to render the form in the browser. */
|
|
28
|
+
uiSchema?: FormUISchema;
|
|
29
|
+
|
|
30
|
+
/** A mapping from form schema to CommonGrants schema. */
|
|
31
|
+
mappingToCommonGrants?: Models.MappingSchema;
|
|
32
|
+
|
|
33
|
+
/** A mapping from CommonGrants schema to form schema. */
|
|
34
|
+
mappingFromCommonGrants?: Models.MappingSchema;
|
|
35
|
+
|
|
36
|
+
/** Custom attributes about the form itself, not custom fields within the form. */
|
|
37
|
+
customFields?: Record<Fields.CustomField>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// #########################################################
|
|
41
|
+
// FormJsonSchema
|
|
42
|
+
// #########################################################
|
|
43
|
+
|
|
44
|
+
/** A JSON schema used to validate form responses. */
|
|
45
|
+
@example(Examples.Form.formSchema)
|
|
46
|
+
model FormJsonSchema {
|
|
47
|
+
...Record<unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// #########################################################
|
|
51
|
+
// FormUISchema
|
|
52
|
+
// #########################################################
|
|
53
|
+
|
|
54
|
+
/** A UI schema used to render the form in the browser. */
|
|
55
|
+
@example(Examples.Form.uiSchema)
|
|
56
|
+
model FormUISchema {
|
|
57
|
+
...Record<unknown>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// #########################################################
|
|
61
|
+
// Examples
|
|
62
|
+
// #########################################################
|
|
63
|
+
|
|
64
|
+
namespace Examples.Form {
|
|
65
|
+
const form = #{
|
|
66
|
+
id: "b7c1e2f4-8a3d-4e2a-9c5b-1f2e3d4c5b6a",
|
|
67
|
+
name: "Form A",
|
|
68
|
+
description: "Form A description",
|
|
69
|
+
instructions: "Form A instructions",
|
|
70
|
+
jsonSchema: formSchema,
|
|
71
|
+
uiSchema: uiSchema,
|
|
72
|
+
mappingToCommonGrants: mappingToCommonGrants,
|
|
73
|
+
mappingFromCommonGrants: mappingFromCommonGrants,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const formSchema = #{
|
|
77
|
+
$id: "formA.json",
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: #{
|
|
80
|
+
name: #{ first: #{ type: "string" }, last: #{ type: "string" } },
|
|
81
|
+
email: #{ type: "string" },
|
|
82
|
+
phone: #{ type: "string" },
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const uiSchema = #{
|
|
87
|
+
type: "VerticalLayout",
|
|
88
|
+
elements: #[
|
|
89
|
+
#{
|
|
90
|
+
type: "Group",
|
|
91
|
+
label: "Name",
|
|
92
|
+
elements: #[
|
|
93
|
+
#{ type: "Control", scope: "#/properties/name/first" },
|
|
94
|
+
#{ type: "Control", scope: "#/properties/name/last" }
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
#{ type: "Control", scope: "#/properties/email" },
|
|
98
|
+
#{ type: "Control", scope: "#/properties/phone" }
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const mappingToCommonGrants = #{
|
|
103
|
+
name: #{
|
|
104
|
+
firstName: #{ field: "name.first" },
|
|
105
|
+
lastName: #{ field: "name.last" },
|
|
106
|
+
},
|
|
107
|
+
emails: #{ primary: #{ field: "email" } },
|
|
108
|
+
phones: #{ primary: #{ field: "phone" } },
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const mappingFromCommonGrants = #{
|
|
112
|
+
name: #{
|
|
113
|
+
first: #{ field: "name.firstName" },
|
|
114
|
+
last: #{ field: "name.lastName" },
|
|
115
|
+
},
|
|
116
|
+
email: #{ field: "emails.primary" },
|
|
117
|
+
phone: #{ field: "phones.primary" },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -5,7 +5,12 @@ import "@typespec/json-schema";
|
|
|
5
5
|
import "./opportunity/index.tsp";
|
|
6
6
|
import "./organization.tsp";
|
|
7
7
|
import "./person.tsp";
|
|
8
|
+
import "./proposal.tsp";
|
|
8
9
|
import "./application.tsp";
|
|
10
|
+
import "./competition.tsp";
|
|
11
|
+
import "./form.tsp";
|
|
12
|
+
import "./form-response.tsp";
|
|
13
|
+
import "./mapping.tsp";
|
|
9
14
|
|
|
10
15
|
using TypeSpec.JsonSchema;
|
|
11
16
|
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
namespace CommonGrants.Models;
|
|
2
|
+
|
|
3
|
+
// #########################################################
|
|
4
|
+
// Mapping
|
|
5
|
+
// #########################################################
|
|
6
|
+
|
|
7
|
+
/** A mapping schema for translating data from one schema to another.
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
*
|
|
11
|
+
* The following mapping:
|
|
12
|
+
*
|
|
13
|
+
* ```json
|
|
14
|
+
* {
|
|
15
|
+
* "id": { "const": "123" },
|
|
16
|
+
* "opportunity": {
|
|
17
|
+
* "status": {
|
|
18
|
+
* "switch": {
|
|
19
|
+
* "field": "opportunity_status",
|
|
20
|
+
* "case": { "active": "open", "inactive": "closed" },
|
|
21
|
+
* "default": "custom",
|
|
22
|
+
* },
|
|
23
|
+
* },
|
|
24
|
+
* "amount": { "field": "opportunity_amount" },
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Will translate the following data:
|
|
30
|
+
*
|
|
31
|
+
* ```json
|
|
32
|
+
* {
|
|
33
|
+
* "id": "123",
|
|
34
|
+
* "opportunity_status": "active",
|
|
35
|
+
* "opportunity_amount": 100,
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* To the following data:
|
|
40
|
+
*
|
|
41
|
+
* ```json
|
|
42
|
+
* {
|
|
43
|
+
* "id": "123",
|
|
44
|
+
* "opportunity": { "status": "open", "amount": 100 },
|
|
45
|
+
* }
|
|
46
|
+
*/
|
|
47
|
+
@example(Examples.Mapping.flatRenaming)
|
|
48
|
+
@example(Examples.Mapping.nestedRenaming)
|
|
49
|
+
@example(Examples.Mapping.simpleSwitch)
|
|
50
|
+
@example(Examples.Mapping.nestedSwitch)
|
|
51
|
+
@example(Examples.Mapping.withLiteralValues)
|
|
52
|
+
model MappingSchema {
|
|
53
|
+
...Record<MappingFunction | unknown>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// #########################################################
|
|
57
|
+
// MappingFunctions
|
|
58
|
+
// #########################################################
|
|
59
|
+
|
|
60
|
+
/** The set of supported mapping functions. */
|
|
61
|
+
union MappingFunction {
|
|
62
|
+
`const`: MappingConstantFunction,
|
|
63
|
+
field: MappingFieldFunction,
|
|
64
|
+
switch: MappingSwitchFunction,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// #########################################################
|
|
68
|
+
// MappingLiteralFunction
|
|
69
|
+
// #########################################################
|
|
70
|
+
|
|
71
|
+
/** Returns a constant value. */
|
|
72
|
+
model MappingConstantFunction {
|
|
73
|
+
`const`: unknown;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// #########################################################
|
|
77
|
+
// MappingFieldFunction
|
|
78
|
+
// #########################################################
|
|
79
|
+
|
|
80
|
+
/** Returns the value of a field in the source data. */
|
|
81
|
+
model MappingFieldFunction {
|
|
82
|
+
field: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// #########################################################
|
|
86
|
+
// MappingSwitchFunction
|
|
87
|
+
// #########################################################
|
|
88
|
+
|
|
89
|
+
/** Returns a new value based on the value of a field in the source data using a switch statement. */
|
|
90
|
+
model MappingSwitchFunction {
|
|
91
|
+
/** The field in the source data to switch on. */
|
|
92
|
+
field: string;
|
|
93
|
+
|
|
94
|
+
/** An object mapping source field values to desired output values. */
|
|
95
|
+
case: Record<unknown>;
|
|
96
|
+
|
|
97
|
+
/** The default value to output if no case matches the source field value. */
|
|
98
|
+
default?: unknown;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// #########################################################
|
|
102
|
+
// Examples
|
|
103
|
+
// #########################################################
|
|
104
|
+
|
|
105
|
+
namespace Examples.Mapping {
|
|
106
|
+
const flatRenaming = #{
|
|
107
|
+
opportunityStatus: #{ field: "opportunity_status" },
|
|
108
|
+
opportunityAmount: #{ field: "opportunity_amount" },
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const nestedRenaming = #{
|
|
112
|
+
id: #{ field: "opportunity_id" },
|
|
113
|
+
opportunity: #{
|
|
114
|
+
status: #{ field: "opportunity_status" },
|
|
115
|
+
amount: #{ field: "opportunity_amount" },
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const simpleSwitch = #{
|
|
120
|
+
opportunityAmount: #{ field: "opportunity_amount" },
|
|
121
|
+
opportunityStatus: #{
|
|
122
|
+
switch: #{
|
|
123
|
+
field: "opportunity_status",
|
|
124
|
+
case: #{ active: "open", inactive: "closed" },
|
|
125
|
+
default: "custom",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const nestedSwitch = #{
|
|
131
|
+
opportunity: #{
|
|
132
|
+
status: #{
|
|
133
|
+
switch: #{
|
|
134
|
+
field: "opportunity_status",
|
|
135
|
+
case: #{ active: "open", inactive: "closed" },
|
|
136
|
+
default: "custom",
|
|
137
|
+
},
|
|
138
|
+
amount: #{ field: "opportunity_amount" },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const withLiteralValues = #{
|
|
144
|
+
id: #{ `const`: "123" },
|
|
145
|
+
opportunity: #{
|
|
146
|
+
status: #{
|
|
147
|
+
switch: #{
|
|
148
|
+
field: "opportunity_status",
|
|
149
|
+
case: #{ active: "open", inactive: "closed" },
|
|
150
|
+
default: "custom",
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
amount: #{ field: "opportunity_amount" },
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const formToCommonGrants = #{
|
|
158
|
+
name: #{ field: "name" },
|
|
159
|
+
email: #{ field: "email" },
|
|
160
|
+
phone: #{ field: "phone" },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
namespace CommonGrants.Models;
|
|
2
|
+
|
|
3
|
+
// #########################################################
|
|
4
|
+
// ProposalBase
|
|
5
|
+
// #########################################################
|
|
6
|
+
|
|
7
|
+
/** A proposal for funding. */
|
|
8
|
+
@example(Examples.Proposal.exampleProposal)
|
|
9
|
+
model ProposalBase {
|
|
10
|
+
/** The title of the proposal and/or the project requesting funding. */
|
|
11
|
+
title?: string;
|
|
12
|
+
|
|
13
|
+
/** The description of the proposal and/or the project requesting funding. */
|
|
14
|
+
description?: string;
|
|
15
|
+
|
|
16
|
+
/** The amount of money requested. */
|
|
17
|
+
amountRequested?: Fields.Money;
|
|
18
|
+
|
|
19
|
+
/** The key dates for the project. */
|
|
20
|
+
projectTimeline?: ProjectTimeline;
|
|
21
|
+
|
|
22
|
+
/** The opportunity to which this proposal is related */
|
|
23
|
+
opportunity?: ProposalOpportunity;
|
|
24
|
+
|
|
25
|
+
/** The organization that is requesting funding. */
|
|
26
|
+
organizations?: ProposalOrgs;
|
|
27
|
+
|
|
28
|
+
/** The point of contact for the project. */
|
|
29
|
+
contacts?: ProposalContacts;
|
|
30
|
+
|
|
31
|
+
/** The project's custom fields. */
|
|
32
|
+
customFields?: Record<Fields.CustomField>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// #########################################################
|
|
36
|
+
// ProposalOpportunity
|
|
37
|
+
// #########################################################
|
|
38
|
+
|
|
39
|
+
/** The opportunity to which this proposal is related */
|
|
40
|
+
model ProposalOpportunity {
|
|
41
|
+
/** The opportunity's unique identifier. */
|
|
42
|
+
id: Types.uuid;
|
|
43
|
+
|
|
44
|
+
/** The opportunity's name. */
|
|
45
|
+
title?: string;
|
|
46
|
+
|
|
47
|
+
/** The opportunity's custom fields. */
|
|
48
|
+
customFields?: Record<Fields.CustomField>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// #########################################################
|
|
52
|
+
// ProjectTimeline
|
|
53
|
+
// #########################################################
|
|
54
|
+
|
|
55
|
+
model ProjectTimeline {
|
|
56
|
+
/** The start date of the period for which the funding is requested. */
|
|
57
|
+
startDate?: Fields.Event;
|
|
58
|
+
|
|
59
|
+
/** The end date of the period for which the funding is requested. */
|
|
60
|
+
endDate?: Fields.Event;
|
|
61
|
+
|
|
62
|
+
/** The key dates for the project. */
|
|
63
|
+
otherDates?: Record<Fields.Event>;
|
|
64
|
+
|
|
65
|
+
/** Details about the timeline that don't fit into the other fields. */
|
|
66
|
+
timelineDetails?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// #########################################################
|
|
70
|
+
// ProjectContacts
|
|
71
|
+
// #########################################################
|
|
72
|
+
|
|
73
|
+
model ProposalContacts {
|
|
74
|
+
/** The primary point of contact for the proposal. */
|
|
75
|
+
primary: PersonBase;
|
|
76
|
+
|
|
77
|
+
/** Other points of contact for the proposal. For example, key personnel, authorized representatives, etc. */
|
|
78
|
+
otherContacts?: Record<PersonBase>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// #########################################################
|
|
82
|
+
// ProposalOrgs
|
|
83
|
+
// #########################################################
|
|
84
|
+
|
|
85
|
+
model ProposalOrgs {
|
|
86
|
+
/** The primary organization that is requesting funding. */
|
|
87
|
+
primary: OrganizationBase;
|
|
88
|
+
|
|
89
|
+
/** Other organizations that are supporting the proposal. For example, a fiscal sponsor, partners, etc. */
|
|
90
|
+
otherOrgs?: Record<OrganizationBase>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// #########################################################
|
|
94
|
+
// Examples
|
|
95
|
+
// #########################################################
|
|
96
|
+
|
|
97
|
+
namespace Examples.Proposal {
|
|
98
|
+
const exampleProposal = #{
|
|
99
|
+
title: "Example Project",
|
|
100
|
+
description: "Example project to serve community needs.",
|
|
101
|
+
amountRequested: #{ amount: "100000", currency: "USD" },
|
|
102
|
+
opportunity: exampleOpportunity,
|
|
103
|
+
projectTimeline: timeline,
|
|
104
|
+
contacts: contacts,
|
|
105
|
+
organizations: organizations,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// #####################################
|
|
109
|
+
// Opportunity
|
|
110
|
+
// #####################################
|
|
111
|
+
|
|
112
|
+
const exampleOpportunity = #{
|
|
113
|
+
id: "083b4567-e89d-42c8-a439-6c1234567890",
|
|
114
|
+
title: "Example Opportunity",
|
|
115
|
+
customFields: #{ agency: Fields.Examples.CustomField.agency },
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// #####################################
|
|
119
|
+
// ProjectTimeline
|
|
120
|
+
// #####################################
|
|
121
|
+
|
|
122
|
+
const timeline = #{
|
|
123
|
+
startDate: #{
|
|
124
|
+
name: "Project Start Date",
|
|
125
|
+
eventType: Fields.EventType.singleDate,
|
|
126
|
+
date: Types.isoDate.fromISO("2025-01-01"),
|
|
127
|
+
},
|
|
128
|
+
endDate: #{
|
|
129
|
+
name: "Project End Date",
|
|
130
|
+
eventType: Fields.EventType.singleDate,
|
|
131
|
+
date: Types.isoDate.fromISO("2025-12-31"),
|
|
132
|
+
},
|
|
133
|
+
otherDates: #{
|
|
134
|
+
evaluationPeriod: #{
|
|
135
|
+
name: "Evaluation Period",
|
|
136
|
+
eventType: Fields.EventType.dateRange,
|
|
137
|
+
startDate: Types.isoDate.fromISO("2025-07-01"),
|
|
138
|
+
endDate: Types.isoDate.fromISO("2025-08-31"),
|
|
139
|
+
description: "The period during which the evaluation will be conducted.",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// #####################################
|
|
145
|
+
// Contacts
|
|
146
|
+
// #####################################
|
|
147
|
+
|
|
148
|
+
const contacts = #{
|
|
149
|
+
primary: Examples.Person.examplePerson,
|
|
150
|
+
otherContacts: #{
|
|
151
|
+
principalInvestigator: #{
|
|
152
|
+
name: #{ prefix: "Dr.", firstName: "Alicia", lastName: "Williams" },
|
|
153
|
+
emails: #{ primary: "alicia.williams@example.com" },
|
|
154
|
+
},
|
|
155
|
+
authorizedRepresentative: #{
|
|
156
|
+
name: #{ firstName: "John", lastName: "Doe" },
|
|
157
|
+
emails: #{ primary: "john.doe@example.com" },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// #####################################
|
|
163
|
+
// Organizations
|
|
164
|
+
// #####################################
|
|
165
|
+
|
|
166
|
+
const organizations = #{
|
|
167
|
+
primary: Examples.Organization.exampleOrg,
|
|
168
|
+
otherOrgs: #{
|
|
169
|
+
fiscalSponsor: Examples.Organization.exampleOrg,
|
|
170
|
+
partner: Examples.Organization.exampleOrg,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -144,3 +144,19 @@ model Filtered<ItemsT, FilterT> extends Success {
|
|
|
144
144
|
errors?: string[];
|
|
145
145
|
};
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
// ############################################################################
|
|
149
|
+
// 201 response
|
|
150
|
+
// ############################################################################
|
|
151
|
+
|
|
152
|
+
/** A 201 response with data
|
|
153
|
+
*
|
|
154
|
+
* @template T The schema for the value of the `"data"` property in this response
|
|
155
|
+
*/
|
|
156
|
+
model Created<T> extends Success {
|
|
157
|
+
// Inherit the 201 status code
|
|
158
|
+
...Http.CreatedResponse;
|
|
159
|
+
|
|
160
|
+
/** Response data */
|
|
161
|
+
data: T;
|
|
162
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import "../responses/index.tsp";
|
|
2
|
+
|
|
3
|
+
// Define the top-level namespace for CommonGrants routes
|
|
4
|
+
namespace CommonGrants.Routes;
|
|
5
|
+
|
|
6
|
+
// Expose the contents of the Http and Rest namespaces
|
|
7
|
+
// these include the decorators @route, @get, etc.
|
|
8
|
+
using TypeSpec.Http;
|
|
9
|
+
|
|
10
|
+
/** A re-usable interface for an Applications router
|
|
11
|
+
*
|
|
12
|
+
* To implement this interface, we recommend declaring a namespace,
|
|
13
|
+
* instantiating the router using `alias` (instead of `extends`),
|
|
14
|
+
* and decorating the namespace with `@route` and `@tag` since they aren't
|
|
15
|
+
* inherited directly from the interface.
|
|
16
|
+
*/
|
|
17
|
+
interface Applications {
|
|
18
|
+
// ##############################
|
|
19
|
+
// Start an application
|
|
20
|
+
// ##############################
|
|
21
|
+
|
|
22
|
+
@summary("Start an application")
|
|
23
|
+
@doc("Start a draft application for a given competition")
|
|
24
|
+
@post
|
|
25
|
+
@route("/start")
|
|
26
|
+
startApplication(
|
|
27
|
+
/** The ID of the competition to start an application for */
|
|
28
|
+
competitionId: Types.uuid,
|
|
29
|
+
|
|
30
|
+
/** The ID of the organization to start an application for, if applying on behalf of an organization */
|
|
31
|
+
organizationId?: Types.uuid,
|
|
32
|
+
|
|
33
|
+
/** The name of the application */
|
|
34
|
+
name: string,
|
|
35
|
+
): Responses.Created<Models.ApplicationBase> | Responses.Unauthorized;
|
|
36
|
+
|
|
37
|
+
// ###############################
|
|
38
|
+
// Get an application
|
|
39
|
+
// ###############################
|
|
40
|
+
|
|
41
|
+
@summary("View an application")
|
|
42
|
+
@doc("View an application for a given competition with draft form responses")
|
|
43
|
+
@get
|
|
44
|
+
@route("/{id}")
|
|
45
|
+
getApplication(
|
|
46
|
+
/** The ID of the application to get */
|
|
47
|
+
@path id: Types.uuid,
|
|
48
|
+
): Responses.Ok<Models.ApplicationBase> | Responses.NotFound | Responses.Unauthorized;
|
|
49
|
+
|
|
50
|
+
// ###############################
|
|
51
|
+
// Submit an application
|
|
52
|
+
// ###############################
|
|
53
|
+
|
|
54
|
+
@summary("Submit an application")
|
|
55
|
+
@doc("Submit an application for a given competition")
|
|
56
|
+
@post
|
|
57
|
+
@route("/{id}/submit")
|
|
58
|
+
submitApplication(
|
|
59
|
+
/** The ID of the application to submit */
|
|
60
|
+
@path id: Types.uuid,
|
|
61
|
+
): Http.NoContentResponse | Responses.NotFound | Responses.Unauthorized;
|
|
62
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import "../responses/index.tsp";
|
|
2
|
+
|
|
3
|
+
// Define the top-level namespace for CommonGrants routes
|
|
4
|
+
namespace CommonGrants.Routes;
|
|
5
|
+
|
|
6
|
+
// Expose the contents of the Http and Rest namespaces
|
|
7
|
+
// these include the decorators @route, @get, etc.
|
|
8
|
+
using TypeSpec.Http;
|
|
9
|
+
|
|
10
|
+
/** A re-usable interface for a Competitions router
|
|
11
|
+
*
|
|
12
|
+
* To implement this interface, we recommend declaring a namespace,
|
|
13
|
+
* instantiating the router using `alias` (instead of `extends`),
|
|
14
|
+
* and decorating the namespace with `@route` and `@tag` since they aren't
|
|
15
|
+
* inherited directly from the interface.
|
|
16
|
+
*/
|
|
17
|
+
interface Competitions {
|
|
18
|
+
// ###############################
|
|
19
|
+
// View competition details
|
|
20
|
+
// ###############################
|
|
21
|
+
|
|
22
|
+
@summary("View competition details")
|
|
23
|
+
@doc("View additional details about a competition for a given opportunity. A competition is an application process for a funding opportunity, often with a distinct set of forms and key dates.")
|
|
24
|
+
@get
|
|
25
|
+
@route("/competitions/{id}")
|
|
26
|
+
read(
|
|
27
|
+
/** The ID of the competition to get */
|
|
28
|
+
@path id: Types.uuid,
|
|
29
|
+
): Responses.Ok<Models.CompetitionBase> | Responses.NotFound;
|
|
30
|
+
|
|
31
|
+
// ###############################
|
|
32
|
+
// Apply to a competition
|
|
33
|
+
// ###############################
|
|
34
|
+
|
|
35
|
+
@summary("Apply to a competition")
|
|
36
|
+
@doc("Apply to a given competition with all of the required information")
|
|
37
|
+
@post
|
|
38
|
+
@route("/competitions/{id}/apply")
|
|
39
|
+
apply(
|
|
40
|
+
/** The ID of the competition to apply to */
|
|
41
|
+
@path id: Types.uuid,
|
|
42
|
+
|
|
43
|
+
/** The application to apply to the competition */
|
|
44
|
+
@body application: Models.ApplicationBase,
|
|
45
|
+
): Responses.Created<Models.ApplicationBase> | Responses.NotFound | Responses.Unauthorized;
|
|
46
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import "../responses/index.tsp";
|
|
2
|
+
|
|
3
|
+
// Define the top-level namespace for CommonGrants routes
|
|
4
|
+
namespace CommonGrants.Routes;
|
|
5
|
+
|
|
6
|
+
// Expose the contents of the Http and Rest namespaces
|
|
7
|
+
// these include the decorators @route, @get, etc.
|
|
8
|
+
using TypeSpec.Http;
|
|
9
|
+
|
|
10
|
+
/** A re-usable interface for an Applications router
|
|
11
|
+
*
|
|
12
|
+
* To implement this interface, we recommend declaring a namespace,
|
|
13
|
+
* instantiating the router using `alias` (instead of `extends`),
|
|
14
|
+
* and decorating the namespace with `@route` and `@tag` since they aren't
|
|
15
|
+
* inherited directly from the interface.
|
|
16
|
+
*/
|
|
17
|
+
interface FormResponses {
|
|
18
|
+
// ###############################
|
|
19
|
+
// Update form response
|
|
20
|
+
// ###############################
|
|
21
|
+
|
|
22
|
+
@summary("Respond to a form")
|
|
23
|
+
@doc("Update the response to a given form on an application")
|
|
24
|
+
@put
|
|
25
|
+
@route("/{appId}/forms/{formId}")
|
|
26
|
+
setFormResponse(
|
|
27
|
+
/** The ID of the application to get */
|
|
28
|
+
@path appId: Types.uuid,
|
|
29
|
+
|
|
30
|
+
/** The ID of the form to update */
|
|
31
|
+
@path formId: Types.uuid,
|
|
32
|
+
|
|
33
|
+
/** The response to the form */
|
|
34
|
+
@body response: Record<unknown>,
|
|
35
|
+
): Responses.Ok<Models.AppFormResponse>;
|
|
36
|
+
|
|
37
|
+
// ###############################
|
|
38
|
+
// Get form response
|
|
39
|
+
// ###############################
|
|
40
|
+
|
|
41
|
+
@summary("Get a form response")
|
|
42
|
+
@doc("Get the response to a given form on an application")
|
|
43
|
+
@get
|
|
44
|
+
@route("/{appId}/forms/{formId}")
|
|
45
|
+
getFormResponse(
|
|
46
|
+
/** The ID of the application to get */
|
|
47
|
+
@path appId: Types.uuid,
|
|
48
|
+
|
|
49
|
+
/** The ID of the form to get */
|
|
50
|
+
@path formId: Types.uuid,
|
|
51
|
+
): Responses.Ok<Models.AppFormResponse>;
|
|
52
|
+
}
|
|
@@ -3,6 +3,9 @@ import "@typespec/rest";
|
|
|
3
3
|
|
|
4
4
|
// Import individual route files to provide a consistent interface
|
|
5
5
|
import "./opportunities.tsp";
|
|
6
|
+
import "./applications.tsp";
|
|
7
|
+
import "./form-responses.tsp";
|
|
8
|
+
import "./competitions.tsp";
|
|
6
9
|
|
|
7
10
|
/** A series of routing interfaces for CommonGrants API endpoints
|
|
8
11
|
*
|
package/lib/main.tsp
CHANGED
|
@@ -3,7 +3,19 @@
|
|
|
3
3
|
// https://typespec.io/docs/extending-typespec/basics/#h-add-your-main-typespec-file
|
|
4
4
|
import "../dist/src/index.js";
|
|
5
5
|
|
|
6
|
+
// Import the core typespec files
|
|
6
7
|
import "./core/index.tsp";
|
|
7
8
|
import "./api.tsp";
|
|
8
9
|
|
|
10
|
+
// Import the versioning package
|
|
11
|
+
import "@typespec/versioning";
|
|
12
|
+
|
|
13
|
+
using Versioning;
|
|
14
|
+
|
|
15
|
+
@versioned(Versions)
|
|
9
16
|
namespace CommonGrants;
|
|
17
|
+
|
|
18
|
+
enum Versions {
|
|
19
|
+
v0_1: "0.1.0",
|
|
20
|
+
v0_2: "0.2.0",
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@common-grants/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-alpha.1",
|
|
4
4
|
"description": "TypeSpec library for defining grant opportunity data models and APIs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
"@typespec/http": "^1.1.0",
|
|
56
56
|
"@typespec/json-schema": "1.0.0",
|
|
57
57
|
"@typespec/openapi3": "1.0.0",
|
|
58
|
-
"@typespec/rest": "0.70.0"
|
|
58
|
+
"@typespec/rest": "0.70.0",
|
|
59
|
+
"@typespec/versioning": "^0.70.0"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"@types/node": "^20.10.6",
|