@common-grants/core 0.1.0 → 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 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",
@@ -2,7 +2,7 @@ import "../types.tsp";
2
2
 
3
3
  namespace CommonGrants.Fields;
4
4
 
5
- using CommonGrants.Types;
5
+ using Types;
6
6
 
7
7
  // ########################################
8
8
  // Event type
@@ -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
+ }
@@ -7,6 +7,7 @@ import "./name.tsp";
7
7
  import "./phone.tsp";
8
8
  import "./pcs.tsp";
9
9
  import "./email.tsp";
10
+ import "./file.tsp";
10
11
 
11
12
  using TypeSpec.JsonSchema;
12
13
 
@@ -2,7 +2,7 @@ import "../types.tsp";
2
2
 
3
3
  namespace CommonGrants.Fields;
4
4
 
5
- using CommonGrants.Types;
5
+ using Types;
6
6
 
7
7
  // ########################################
8
8
  // Model definition
@@ -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 application's unique identifier. */
4
+ /** The unique identifier for the application */
9
5
  id: Types.uuid;
10
6
 
11
- /** The application's status. */
12
- status?: AppStatus;
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 organization that is applying for the grant. */
18
- organization?: OrganizationBase;
10
+ /** The unique identifier for the competition */
11
+ competitionId: Types.uuid;
19
12
 
20
- /** The person who is applying for the grant. */
21
- pointOfContact?: PersonBase;
13
+ /** The form responses for the application */
14
+ formResponses: Record<AppFormResponse>;
22
15
 
23
- /** The application's proposal for funding. */
24
- proposal?: AppProposal;
16
+ /** The status of the application */
17
+ status: AppStatus;
25
18
 
26
- /** The opportunity being applied to. */
27
- opportunity?: AppOpportunity;
19
+ /** The date and time the application was submitted */
20
+ submittedAt?: utcDateTime | null;
28
21
 
29
- /** The application's custom fields. */
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
- submitted,
57
- approved,
58
- rejected,
59
- custom,
60
- }
52
+ /** The application is a draft */
53
+ draft,
61
54
 
62
- // #########################################################
63
- // AppProject
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 start date of the period for which the funding is requested. */
79
- periodStartDate?: Types.isoDate;
58
+ /** The application has been accepted */
59
+ accepted,
80
60
 
81
- /** The end date of the period for which the funding is requested. */
82
- periodEndDate?: Types.isoDate;
61
+ /** The application has been rejected */
62
+ rejected,
83
63
 
84
- /** The project's custom fields. */
85
- customFields?: Record<Fields.CustomField>;
64
+ /** The application has a custom status */
65
+ custom,
86
66
  }
87
67
 
88
68
  // #########################################################
89
- // AppOpportunity
69
+ // ApplicationFormResponse
90
70
  // #########################################################
91
71
 
92
- /** The opportunity to which this application is related */
93
- model AppOpportunity {
94
- /** The opportunity's unique identifier. */
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: "Application has been submitted.",
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
+ }
@@ -6,8 +6,8 @@ import "../../types.tsp";
6
6
 
7
7
  namespace CommonGrants.Models;
8
8
 
9
- using CommonGrants.Fields;
10
- using CommonGrants.Types;
9
+ using Fields;
10
+ using Types;
11
11
 
12
12
  // ########################################
13
13
  // Model definition
@@ -3,7 +3,7 @@ import "../../fields/index.tsp";
3
3
 
4
4
  namespace CommonGrants.Models;
5
5
 
6
- using CommonGrants.Fields;
6
+ using Fields;
7
7
 
8
8
  // ########################################
9
9
  // Model definition
@@ -1,6 +1,3 @@
1
- import "@typespec/json-schema";
2
- import "@typespec/openapi3";
3
-
4
1
  namespace CommonGrants.Models;
5
2
 
6
3
  // ########################################
@@ -3,8 +3,8 @@ import "../../types.tsp";
3
3
 
4
4
  namespace CommonGrants.Models;
5
5
 
6
- using CommonGrants.Fields;
7
- using CommonGrants.Types;
6
+ using Fields;
7
+ using Types;
8
8
 
9
9
  // ########################################
10
10
  // Model definition
@@ -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
+ }
@@ -2,6 +2,7 @@ import "@typespec/http";
2
2
 
3
3
  using TypeSpec.Http;
4
4
  using TypeSpec.JsonSchema;
5
+
5
6
  /** Models and utilities for pagination */
6
7
  @jsonSchema
7
8
  namespace CommonGrants.Pagination;
@@ -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.1.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",
@@ -51,11 +51,12 @@
51
51
  "author": "CommonGrants",
52
52
  "license": "CC0-1.0",
53
53
  "peerDependencies": {
54
- "@typespec/compiler": "^0.66.0",
55
- "@typespec/http": "^0.66.0",
56
- "@typespec/json-schema": "^0.66.0",
57
- "@typespec/openapi3": "^0.66.0",
58
- "@typespec/rest": "^0.66.0"
54
+ "@typespec/compiler": "^1.1.0",
55
+ "@typespec/http": "^1.1.0",
56
+ "@typespec/json-schema": "1.0.0",
57
+ "@typespec/openapi3": "1.0.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",