@common-grants/core 0.1.1 → 0.2.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.
@@ -0,0 +1,99 @@
1
+ namespace CommonGrants.Models;
2
+
3
+ /** The base model for a form response */
4
+ @example(Examples.FormResponse.formResponse)
5
+ model FormResponseBase {
6
+ /** The unique identifier for the form response */
7
+ id: Types.uuid;
8
+
9
+ /** The form being responded to */
10
+ formId: Types.uuid;
11
+
12
+ /** The response to the form */
13
+ response: Record<unknown>;
14
+
15
+ /** The status of the form response */
16
+ status: FormResponseStatus;
17
+
18
+ /** The validation errors for the form response */
19
+ validationErrors?: Array<unknown>;
20
+
21
+ /** Custom attributes about the form response */
22
+ customFields?: Record<Fields.CustomField>;
23
+
24
+ /** The system metadata for the form response */
25
+ ...Fields.SystemMetadata;
26
+ }
27
+
28
+ // #########################################################
29
+ // FormResponseStatus
30
+ // #########################################################
31
+
32
+ /** The status of the form response */
33
+ @example(Examples.FormResponse.inProgressStatus)
34
+ model FormResponseStatus {
35
+ /** The status of the form response, from a predefined set of options */
36
+ value: FormResponseStatusOptions;
37
+
38
+ /** A custom value for the status */
39
+ customValue?: string;
40
+
41
+ /** A human-readable description of the status */
42
+ description?: string;
43
+ }
44
+
45
+ // #########################################################
46
+ // FormResponseStatusOptions
47
+ // #########################################################
48
+
49
+ /** The set of values accepted for form response status:
50
+ * - `notStarted`: The form response has not been started
51
+ * - `inProgress`: The form response is in progress
52
+ * - `complete`: The form response is complete, meaning all required fields have been filled out and there are no validation errors
53
+ * - `custom`: A custom status
54
+ */
55
+ enum FormResponseStatusOptions {
56
+ notStarted,
57
+ inProgress,
58
+ complete,
59
+ }
60
+
61
+ // #########################################################
62
+ // Examples
63
+ // #########################################################
64
+
65
+ namespace Examples.FormResponse {
66
+ const inProgressStatus = #{
67
+ value: FormResponseStatusOptions.inProgress,
68
+ description: "The form response is in progress",
69
+ };
70
+
71
+ const completeStatus = #{
72
+ value: FormResponseStatusOptions.complete,
73
+ description: "The form response is complete",
74
+ };
75
+
76
+ const formResponse = #{
77
+ id: "123e4567-e89b-12d3-a456-426614174000",
78
+ formId: "123e4567-e89b-12d3-a456-426614174000",
79
+ response: #{
80
+ firstName: "John",
81
+ lastName: "Doe",
82
+ email: "john.doe@example.com",
83
+ phone: "123-456-7890",
84
+ address: #{
85
+ street: "123 Main St",
86
+ city: "Anytown",
87
+ state: "CA",
88
+ zip: "12345",
89
+ country: null,
90
+ },
91
+ },
92
+ status: inProgressStatus,
93
+ validationErrors: #[
94
+ #{ field: "address.country", message: "Country is required" }
95
+ ],
96
+ createdAt: utcDateTime.fromISO("2021-01-01T00:00:00Z"),
97
+ lastModifiedAt: utcDateTime.fromISO("2021-01-01T00:00:00Z"),
98
+ };
99
+ }
@@ -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,13 @@ 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";
14
+ import "./applicant-type.tsp";
9
15
 
10
16
  using TypeSpec.JsonSchema;
11
17
 
@@ -0,0 +1,152 @@
1
+ namespace CommonGrants.Models;
2
+
3
+ // #########################################################
4
+ // Mapping
5
+ // #########################################################
6
+
7
+ /** A mapping format 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": "summary.opportunity_status",
20
+ * "case": { "active": "open", "inactive": "closed" },
21
+ * "default": "custom",
22
+ * },
23
+ * },
24
+ * "amount": { "field": "summary.opportunity_amount" },
25
+ * }
26
+ * }
27
+ * ```
28
+ *
29
+ * Will translate the following data:
30
+ *
31
+ * ```json
32
+ * {
33
+ * "id": "123",
34
+ * "summary": {
35
+ * "opportunity_status": "active",
36
+ * "opportunity_amount": 100,
37
+ * },
38
+ * }
39
+ * ```
40
+ *
41
+ * To the following data:
42
+ *
43
+ * ```json
44
+ * {
45
+ * "id": "123",
46
+ * "opportunity": { "status": "open", "amount": 100 },
47
+ * }
48
+ * ```
49
+ */
50
+ @example(Examples.Mapping.flatRenaming)
51
+ @example(Examples.Mapping.nestedRenaming)
52
+ @example(Examples.Mapping.simpleSwitch)
53
+ @example(Examples.Mapping.nestedSwitch)
54
+ @example(Examples.Mapping.withLiteralValues)
55
+ model MappingSchema {
56
+ ...Record<MappingFunction | MappingSchema>;
57
+ }
58
+
59
+ // #########################################################
60
+ // MappingFunctions
61
+ // #########################################################
62
+
63
+ /** The set of supported mapping functions. */
64
+ @example(Examples.Mapping.constId)
65
+ @example(Examples.Mapping.amountField)
66
+ @example(Examples.Mapping.statusSwitch)
67
+ union MappingFunction {
68
+ `const`: MappingConstantFunction,
69
+ field: MappingFieldFunction,
70
+ switch: MappingSwitchFunction,
71
+ }
72
+
73
+ // #########################################################
74
+ // MappingConstantFunction
75
+ // #########################################################
76
+
77
+ /** Returns a constant value. */
78
+ @example(Examples.Mapping.constId)
79
+ model MappingConstantFunction {
80
+ `const`: unknown;
81
+ }
82
+
83
+ // #########################################################
84
+ // MappingFieldFunction
85
+ // #########################################################
86
+
87
+ /** Returns the value of a field in the source data. */
88
+ @example(Examples.Mapping.amountField)
89
+ model MappingFieldFunction {
90
+ field: string;
91
+ }
92
+
93
+ // #########################################################
94
+ // MappingSwitchFunction
95
+ // #########################################################
96
+
97
+ /** Returns a new value based on the value of a field in the source data using a switch statement. */
98
+ @example(Examples.Mapping.statusSwitch)
99
+ model MappingSwitchFunction {
100
+ switch: {
101
+ /** The field in the source data to switch on. */
102
+ field: string;
103
+
104
+ /** An object mapping source field values to desired output values. */
105
+ case: Record<unknown>;
106
+
107
+ /** The default value to output if no case matches the source field value. */
108
+ default?: unknown;
109
+ };
110
+ }
111
+
112
+ // #########################################################
113
+ // Examples
114
+ // #########################################################
115
+
116
+ namespace Examples.Mapping {
117
+ const constId = #{ `const`: "123" };
118
+ const amountField = #{ field: "summary.opportunity_amount" };
119
+ const statusField = #{ field: "summary.opportunity_status" };
120
+ const statusSwitch = #{
121
+ switch: #{
122
+ field: "summary.opportunity_status",
123
+ case: #{ active: "open", inactive: "closed" },
124
+ default: "custom",
125
+ },
126
+ };
127
+
128
+ const flatRenaming = #{
129
+ id: constId,
130
+ opportunityStatus: statusField,
131
+ opportunityAmount: amountField,
132
+ };
133
+
134
+ const nestedRenaming = #{
135
+ id: constId,
136
+ opportunity: #{ status: statusField, amount: amountField },
137
+ };
138
+
139
+ const simpleSwitch = #{
140
+ opportunityAmount: amountField,
141
+ opportunityStatus: statusSwitch,
142
+ };
143
+
144
+ const nestedSwitch = #{
145
+ opportunity: #{ status: statusSwitch, amount: amountField },
146
+ };
147
+
148
+ const withLiteralValues = #{
149
+ id: constId,
150
+ opportunity: #{ status: statusSwitch, amount: amountField },
151
+ };
152
+ }
@@ -62,6 +62,9 @@ model OpportunityBase {
62
62
  /** Key dates for the opportunity, such as when the application opens and closes */
63
63
  keyDates?: OppTimeline;
64
64
 
65
+ /** The type of applicant for the opportunity */
66
+ acceptedApplicantTypes?: ApplicantType[];
67
+
65
68
  /** URL for the original source of the opportunity */
66
69
  source?: url;
67
70
 
@@ -71,3 +74,13 @@ model OpportunityBase {
71
74
  // Spreads the fields from the Metadata model into the Opportunity model
72
75
  ...SystemMetadata;
73
76
  }
77
+
78
+ // ########################################
79
+ // Opportunity details
80
+ // ########################################
81
+
82
+ /** A funding opportunity with additional details, like available competitions. */
83
+ model OpportunityDetails extends OpportunityBase {
84
+ /** The competitions associated with the opportunity */
85
+ competitions?: CompetitionBase[];
86
+ }
@@ -4,7 +4,12 @@ namespace CommonGrants.Models;
4
4
  // Opportunity status options
5
5
  // ########################################
6
6
 
7
- /** The set of values accepted for opportunity status */
7
+ /** The set of values accepted for opportunity status:
8
+ * - `forecasted`: The opportunity is forecasted and not yet open for applications
9
+ * - `open`: The opportunity is open for applications
10
+ * - `closed`: The opportunity is no longer accepting applications
11
+ * - `custom`: A custom status
12
+ */
8
13
  enum OppStatusOptions {
9
14
  forecasted,
10
15
  open,
@@ -20,10 +25,10 @@ enum OppStatusOptions {
20
25
  @example(Examples.OppStatus.custom)
21
26
  @example(Examples.OppStatus.default)
22
27
  model OppStatus {
23
- /** The status value, from a predefined set of options */
28
+ /** The status of the opportunity, from a predefined set of options */
24
29
  value: OppStatusOptions;
25
30
 
26
- /** A custom status value */
31
+ /** A custom value for the status */
27
32
  customValue?: string;
28
33
 
29
34
  /** A human-readable description of the status */
@@ -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
+ }
@@ -28,3 +28,14 @@ model Error {
28
28
 
29
29
  alias Unauthorized = Error & Http.UnauthorizedResponse;
30
30
  alias NotFound = Error & Http.NotFoundResponse;
31
+
32
+ @example(#{
33
+ status: 400,
34
+ message: "Application submission failed due to validation errors",
35
+ errors: #[#{ field: "formA.name", message: "Name is required" }],
36
+ })
37
+ @doc("A failure to submit an application due to validation errors")
38
+ model ApplicationSubmissionError extends Error {
39
+ @example(400)
40
+ status: 400;
41
+ }
@@ -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
+ }