@common-grants/core 0.1.0-alpha.6 → 0.1.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,16 +48,17 @@ model Agency extends CustomField {
48
48
 
49
49
  @example("Department of Transportation")
50
50
  value: string;
51
- description: "The agency responsible for this opportunity";
52
- }
53
51
 
54
- // Create a map of custom fields
55
- model CustomFields extends CustomFieldMap {
56
- agency: Agency;
52
+ description: "The agency responsible for this opportunity";
57
53
  }
58
54
 
59
- // Create a custom Opportunity type using the template
60
- alias CustomOpportunity = Opportunity<CustomFields>;
55
+ // Extend the `OpportunityBase` model to create a new `CustomOpportunity` model
56
+ // that includes the new `Agency` field in its `customFields` property
57
+ model CustomOpportunity extends OpportunityBase {
58
+ customFields: {
59
+ agency: Agency;
60
+ }
61
+ };
61
62
  ```
62
63
 
63
64
  ### Override default routes
package/lib/main.tsp CHANGED
@@ -9,13 +9,17 @@ import "./routes/index.tsp";
9
9
 
10
10
  using TypeSpec.Http;
11
11
 
12
- // Define the top-level namespace for the library
12
+ /** The base OpenAPI specification for a CommonGrants API
13
+ *
14
+ * In order for an API to be "compliant" with the CommonGrants protocol,
15
+ * it must implement all of the routes included in this base specification.
16
+ */
13
17
  @service({
14
- name: "Base API",
18
+ title: "Base API",
15
19
  })
16
20
  namespace CommonGrants.API;
17
21
 
18
- @tag("Search")
22
+ @tag("Opportunities")
19
23
  @route("/opportunities")
20
24
  namespace Opportunities {
21
25
  alias Router = Routes.Opportunities;
@@ -0,0 +1,27 @@
1
+ namespace CommonGrants.Models;
2
+
3
+ /**
4
+ * A custom enum value, designed to be used as part of a union with known options
5
+ *
6
+ * @example How to use this CustomEnumValue in a union
7
+ *
8
+ * ```typespec
9
+ * union OpportunityStatus {
10
+ * open: {
11
+ * value: "open",
12
+ * description?: "Opportunity is actively accepting applications",
13
+ * },
14
+ * closed: {
15
+ * value: "closed",
16
+ * description?: "Opportunity is no longer accepting applications",
17
+ * },
18
+ * custom: CustomEnumValue,
19
+ * }
20
+ * ```
21
+ */
22
+ @doc("Custom value not included in the standard list of options.")
23
+ model CustomEnumValue {
24
+ value: "custom";
25
+ customValue: string;
26
+ description: string;
27
+ }
@@ -1,7 +1,7 @@
1
1
  namespace CommonGrants.Models;
2
2
 
3
3
  // ########################################
4
- // Model definition
4
+ // Field types definition
5
5
  // ########################################
6
6
 
7
7
  /** The set of JSON schema types supported by a custom field */
@@ -13,9 +13,27 @@ enum CustomFieldType {
13
13
  array,
14
14
  }
15
15
 
16
- /** An object that maps field names to CustomField instances */
17
- model CustomFieldMap is Record<CustomField>;
16
+ // ########################################
17
+ // Models definition
18
+ // ########################################
18
19
 
20
+ /** Model for defining custom fields that are specific to a given implementation.
21
+ *
22
+ * @example How to define a custom field using this model
23
+ *
24
+ * ```typespec
25
+ * model Agency extends CustomField {
26
+ * name: "agency";
27
+ *
28
+ * type: CustomFieldType.string;
29
+ *
30
+ * @example("Department of Transportation")
31
+ * value: string;
32
+ *
33
+ * description?: "The agency responsible for managing this opportunity";
34
+ * }
35
+ * ```
36
+ */
19
37
  @example(
20
38
  CustomFieldExamples.programArea,
21
39
  #{ title: "String field for program area" }
@@ -24,6 +42,7 @@ model CustomFieldMap is Record<CustomField>;
24
42
  CustomFieldExamples.eligibilityTypes,
25
43
  #{ title: "Array field for eligibility types" }
26
44
  )
45
+ @doc("A custom field on a model") // Overrides internal docstrings when emitting OpenAPI
27
46
  model CustomField {
28
47
  /** Name of the custom field */
29
48
  name: string;
@@ -4,10 +4,11 @@ namespace CommonGrants.Models;
4
4
  // Model definition
5
5
  // ########################################
6
6
 
7
+ /** Description of an event that has a date (and possible time) associated with it */
7
8
  @example(EventExamples.deadline, #{ title: "Application deadline with time" })
8
9
  @example(EventExamples.openDate, #{ title: "Opening date without time" })
9
10
  model Event {
10
- /** Name of the timeline event (e.g., 'Open Date', 'Deadline') */
11
+ /** Human-readable name of the event (e.g., 'Application posted', 'Question deadline') */
11
12
  name: string;
12
13
 
13
14
  /** Date of the event in in ISO 8601 format: YYYY-MM-DD */
@@ -16,7 +17,7 @@ model Event {
16
17
  /** Time of the event in ISO 8601 format: HH:MM:SS */
17
18
  time?: isoTime;
18
19
 
19
- /** Detailed description of the timeline event */
20
+ /** Description of what this event represents */
20
21
  description?: string;
21
22
  }
22
23
 
@@ -5,13 +5,38 @@ import "@typespec/json-schema";
5
5
  import "./base.tsp";
6
6
  import "./custom-field.tsp";
7
7
  import "./money.tsp";
8
- import "./funding.tsp";
9
8
  import "./event.tsp";
10
- import "./opportunity.tsp";
9
+ import "./metadata.tsp";
10
+ import "./custom-enum.tsp";
11
+ import "./opportunity/index.tsp";
11
12
 
12
13
  using TypeSpec.JsonSchema;
13
14
 
14
- // Define the top-level namespace for the models
15
- // and emit these models as JSON schemas
16
- @jsonSchema
15
+ /** Namespace for CommonGrants models that can be used in API routes */
16
+ @jsonSchema // and emit these models as JSON schemas
17
17
  namespace CommonGrants.Models;
18
+
19
+ /** The base model for a funding opportunity.
20
+ *
21
+ * It supports customization by extending the `customFields` property.
22
+ *
23
+ * @example How to declare a new Opportunity model with custom fields
24
+ *
25
+ * ```typespec
26
+ * model Agency extends CustomField {
27
+ * type: CustomFieldType.string;
28
+ *
29
+ * @example("Department of Transportation")
30
+ * value: string;
31
+ * }
32
+ *
33
+ * model NewFields extends CustomFieldMap {
34
+ * agency: Agency;
35
+ * }
36
+ *
37
+ * model CustomOpportunity extends OpportunityBase {
38
+ * customFields: NewFields;
39
+ * }
40
+ * ```
41
+ */
42
+ alias OpportunityBase = Opportunity.OpportunityBase;
@@ -0,0 +1,41 @@
1
+ namespace CommonGrants.Models;
2
+
3
+ // ########################################
4
+ // Model definition
5
+ // ########################################
6
+
7
+ /** Standard system-level metadata about a given record.
8
+ *
9
+ * @example How to spread the SystemMetadata props into another record
10
+ *
11
+ * ```typespec
12
+ * model Opportunity {
13
+ * id: uuid;
14
+ * title: string;
15
+ *
16
+ * // Includes SystemMetadata props in the root of the Opportunity model
17
+ * ...SystemMetadata;
18
+ * }
19
+ * ```
20
+ * */
21
+ @example(MetadataExample.system)
22
+ model SystemMetadata {
23
+ /** The timestamp (in UTC) at which the record was created. */
24
+ @visibility(Lifecycle.Read)
25
+ createdAt: utcDateTime;
26
+
27
+ /** The timestamp (in UTC) at which the record was last modified. */
28
+ @visibility(Lifecycle.Read)
29
+ lastModifiedAt: utcDateTime;
30
+ }
31
+
32
+ // ########################################
33
+ // Model examples
34
+ // ########################################
35
+
36
+ namespace MetadataExample {
37
+ const system = #{
38
+ createdAt: utcDateTime.fromISO("2025-01-01T17:01:01"),
39
+ lastModifiedAt: utcDateTime.fromISO("2025-01-02T17:30:00"),
40
+ };
41
+ }
@@ -4,6 +4,7 @@ namespace CommonGrants.Models;
4
4
  // Model definition
5
5
  // ########################################
6
6
 
7
+ /** A monetary amount and the currency in which its denominated */
7
8
  @example(MoneyExamples.usdWithCents, #{ title: "US dollars and cents" })
8
9
  @example(
9
10
  MoneyExamples.euroWithoutCents,
@@ -1,11 +1,12 @@
1
- using TypeSpec.Http;
1
+ import "../index.tsp";
2
2
 
3
- namespace CommonGrants.Models;
3
+ namespace CommonGrants.Models.Opportunity;
4
4
 
5
5
  // ########################################
6
6
  // Model definition
7
7
  // ########################################
8
8
 
9
+ /** Details about the funding available for this opportunity */
9
10
  @example(FundingExamples.allFields, #{ title: "All fields defined" })
10
11
  @example(
11
12
  FundingExamples.awardRange,
@@ -16,11 +17,22 @@ namespace CommonGrants.Models;
16
17
  #{ title: "Total funding limit but no award range" }
17
18
  )
18
19
  model FundingDetails {
20
+ /** Total amount of funding available for this opportunity */
19
21
  totalAmountAvailable?: Money;
22
+
23
+ /** Minimum amount of funding granted per award */
20
24
  minAwardAmount?: Money;
25
+
26
+ /** Maximum amount of funding granted per award */
21
27
  maxAwardAmount?: Money;
28
+
29
+ /** Minimum number of awards granted */
22
30
  minAwardCount?: integer;
31
+
32
+ /** Maximum number of awards granted */
23
33
  maxAwardCount?: integer;
34
+
35
+ /** Estimated number of awards that will be granted */
24
36
  estimatedAwardCount?: integer;
25
37
  }
26
38
 
@@ -1,55 +1,46 @@
1
- namespace CommonGrants.Models;
1
+ import "@typespec/json-schema";
2
+
3
+ import "./status.tsp";
4
+ import "./funding.tsp";
5
+ import "./timeline.tsp";
6
+
7
+ /** Namespace for CommonGrants models that are specific to funding opportunities */
8
+ @JsonSchema.jsonSchema
9
+ namespace CommonGrants.Models.Opportunity;
2
10
 
3
11
  // ########################################
4
12
  // Model definition
5
13
  // ########################################
6
14
 
7
- /** The base model for a funding opportunity.
8
- *
9
- * It supports customization by templating the customFields property.
10
- *
11
- * @template Fields A CustomFieldMap object with user-defined custom fields,
12
- * that should be included in a given implementation of the Opportunity model.
13
- * @example Declare a new Opportunity model with custom fields
14
- *
15
- * ```typespec
16
- * model Agency extends CustomField {
17
- * type: CustomFieldType.string;
18
- * value: string;
19
- * }
20
- *
21
- * model NewFields extends CustomFieldMap {
22
- * agency: Agency;
23
- * }
24
- *
25
- * alias CustomOpportunity = Opportunity<NewFields>;
26
- * ```
27
- */
28
- @example(
29
- OpportunityExamples.minimal,
30
- #{ title: "Minimal opportunity with required fields only" }
31
- )
32
- model Opportunity<Fields = CustomFieldMap> {
15
+ @doc("A funding opportunity") // Overrides internal docstrings when emitting OpenAPI
16
+ model OpportunityBase {
33
17
  /** Globally unique id for the opportunity */
18
+ @visibility(Lifecycle.Read)
34
19
  id: uuid;
35
20
 
36
- /** URL for the original source of the opportunity */
37
- source: url;
38
-
39
21
  /** Title or name of the funding opportunity */
40
22
  title: string;
41
23
 
24
+ /** Status of the opportunity */
25
+ status: OppStatus;
26
+
42
27
  /** Description of the opportunity's purpose and scope */
43
28
  description: string;
44
29
 
45
30
  /** Details about the funding available */
46
31
  fundingDetails: FundingDetails;
47
32
 
48
- /** Key dates and milestones in the application process */
49
- applicationTimeline?: Event[];
33
+ /** Key dates for the opportunity, such as when the application opens and closes */
34
+ keyDates: OppTimeline;
35
+
36
+ /** URL for the original source of the opportunity */
37
+ source?: url;
50
38
 
51
39
  /** Additional custom fields specific to this opportunity */
52
- customFields?: Fields;
40
+ customFields?: Record<CustomField>;
41
+
42
+ // Spreads the fields from the Metadata model into the Opportunity model
43
+ ...SystemMetadata;
53
44
  }
54
45
 
55
46
  // ########################################
@@ -64,11 +55,12 @@ namespace OpportunityExamples {
64
55
  title: "Healthcare Innovation Research Grant",
65
56
  description: "Funding for innovative healthcare delivery solutions",
66
57
  fundingDetails: FundingExamples.allFields,
67
- applicationTimeline: #[EventExamples.openDate, EventExamples.deadline],
58
+ keyDates: #[EventExamples.openDate, EventExamples.deadline],
68
59
  customFields: #{
69
60
  programArea: CustomFieldExamples.programArea,
70
61
  eligibilityType: CustomFieldExamples.programArea,
71
62
  },
63
+ ...MetadataExample.system,
72
64
  };
73
65
 
74
66
  /** A minimal opportunity example with only required fields */
@@ -78,5 +70,6 @@ namespace OpportunityExamples {
78
70
  title: "Small Business Innovation Grant",
79
71
  description: "Supporting small business innovation projects",
80
72
  fundingDetails: FundingExamples.onlyLimit,
73
+ ...MetadataExample.system,
81
74
  };
82
75
  }
@@ -0,0 +1,33 @@
1
+ import "@typespec/json-schema";
2
+ import "@typespec/openapi3";
3
+
4
+ import "../custom-enum.tsp";
5
+
6
+ namespace CommonGrants.Models.Opportunity;
7
+
8
+ /** Union of values accepted for opportunity status */
9
+ @JsonSchema.oneOf
10
+ @OpenAPI.oneOf
11
+ @discriminator("value")
12
+ union OppStatus {
13
+ /** Opportunity is anticipated, but not yet accepting applications */
14
+ forecasted: {
15
+ value: "forecasted",
16
+ description?: "Opportunity is anticipated, but not yet accepting applications",
17
+ },
18
+
19
+ /** Opportunity is actively accepting applications */
20
+ open: {
21
+ value: "open",
22
+ description?: "Opportunity is actively accepting applications",
23
+ },
24
+
25
+ /** Opportunity is no longer accepting applications */
26
+ closed: {
27
+ value: "closed",
28
+ description?: "Opportunity is no longer accepting applications",
29
+ },
30
+
31
+ /** Custom opportunity status */
32
+ custom: CustomEnumValue,
33
+ }
@@ -0,0 +1,42 @@
1
+ import "../event.tsp";
2
+ import "../custom-enum.tsp";
3
+
4
+ namespace CommonGrants.Models.Opportunity;
5
+
6
+ // ########################################
7
+ // Model definition
8
+ // ########################################
9
+
10
+ /** Key dates in the opportunity's timeline, such as when the application opens and closes */
11
+ @example(TimelineExamples.opportunity)
12
+ model OppTimeline {
13
+ /** The date (and time) at which the opportunity begins accepting applications */
14
+ appOpens?: Event;
15
+
16
+ /** The final deadline for submitting applications */
17
+ appDeadline?: Event;
18
+
19
+ /** An optional map of other key dates in the opportunity timeline
20
+ *
21
+ * Examples might include a deadline for questions, anticipated award date, etc.
22
+ */
23
+ otherDates?: Record<Event>;
24
+ }
25
+
26
+ // ########################################
27
+ // Model examples
28
+ // ########################################
29
+
30
+ namespace TimelineExamples {
31
+ const opportunity = #{
32
+ appOpens: EventExamples.openDate,
33
+ appDeadline: EventExamples.deadline,
34
+ otherDates: #{
35
+ anticipatedAward: #{
36
+ name: "Anticipated award date",
37
+ date: isoDate.fromISO("2025-03-15"),
38
+ description: "When we expect to announce awards for this opportunity.",
39
+ },
40
+ },
41
+ };
42
+ }
@@ -1,6 +1,17 @@
1
1
  namespace Responses.Error;
2
2
 
3
+ /** A standard error response schema, used to create custom error responses
4
+ *
5
+ * @example - How to use this model to create custom error response schemas
6
+ *
7
+ * ```
8
+ * import "@typespec/http"
9
+ *
10
+ * alias Unauthorized = Error & Http.UnauthorizedResponse
11
+ * ```
12
+ */
3
13
  @error
14
+ @doc("A non-2xx response schema")
4
15
  model Error {
5
16
  @example(400)
6
17
  status: int32;
@@ -8,7 +8,24 @@ model Success {
8
8
  message: string;
9
9
  }
10
10
 
11
- /** Template for normal response data */
11
+ /** Template for a 200 response with data
12
+ *
13
+ * @template T The schema for the value of the `"data"` property in this response
14
+ * @example How to specify a custom 200 response model
15
+ *
16
+ * ```typespec
17
+ *
18
+ * // Define a model
19
+ * model CustomModel {
20
+ * id: string;
21
+ * description: string;
22
+ * }
23
+ *
24
+ * // Pass that model to the `Ok` template
25
+ * alias CustomModel200 = Success.Ok<CustomModel>;
26
+ * ```
27
+ */
28
+ @doc("A 200 response with data")
12
29
  model Ok<T> extends Success {
13
30
  // Inherit the 200 status code
14
31
  ...Http.OkResponse;
@@ -17,7 +34,24 @@ model Ok<T> extends Success {
17
34
  data: T;
18
35
  }
19
36
 
20
- /** Template for paginated responses */
37
+ /** Template for a 200 response with paginated list of items
38
+ *
39
+ * @template T The schema for the value of the `"items"` property in this response
40
+ * @example How to specify a custom paginated response model
41
+ *
42
+ * ```typespec
43
+ *
44
+ * // Define a model
45
+ * model CustomModel {
46
+ * id: string;
47
+ * description: string;
48
+ * }
49
+ *
50
+ * // Pass that model to the `Ok` template
51
+ * alias CustomModelResponse = Success.Paginated<CustomModel>;
52
+ * ```
53
+ */
54
+ @doc("A 200 response with a paginated list of items")
21
55
  model Paginated<T> extends Success {
22
56
  // Inherit the 200 status code
23
57
  ...Http.OkResponse;
@@ -52,24 +52,24 @@ interface Opportunities {
52
52
  /** `GET /opportunities/` Get a paginated list of opportunities
53
53
  *
54
54
  * @template T Type of the paginated response model.
55
- * Must be an extension of Models.Opportunity. Default is Models.Opportunity.
55
+ * Must be an extension of Models.OpportunityBase. Default is Models.OpportunityBase.
56
56
  */
57
57
  @summary("List opportunities")
58
- @doc("Get a paginated list of opportunities")
58
+ @doc("Get a paginated list of opportunities, sorted by `lastModifiedAt` with most recent first.")
59
59
  @list
60
- list<T extends Models.Opportunity = Models.Opportunity>(
60
+ list<T extends Models.OpportunityBase = Models.OpportunityBase>(
61
61
  ...PaginatedQuery,
62
62
  ): Success.Paginated<T> | Error.Unauthorized;
63
63
 
64
64
  /** `GET /opportunities/<id>` View opportunity details
65
65
  *
66
66
  * @template T Type of the response model.
67
- * Must be an extension of Models.Opportunity. Default is Models.Opportunity.
67
+ * Must be an extension of Models.OpportunityBase. Default is Models.OpportunityBase.
68
68
  */
69
69
  @summary("View opportunity")
70
70
  @doc("View additional details about an opportunity")
71
71
  @get
72
- read<T extends Models.Opportunity = Models.Opportunity>(
72
+ read<T extends Models.OpportunityBase = Models.OpportunityBase>(
73
73
  @path id: Models.uuid,
74
74
  ): Success.Ok<T> | Error.NotFound | Error.Unauthorized;
75
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@common-grants/core",
3
- "version": "0.1.0-alpha.6",
3
+ "version": "0.1.0-alpha.7",
4
4
  "description": "TypeSpec library for defining grant opportunity data models and APIs",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -31,7 +31,10 @@
31
31
  "format": "prettier --write . && tsp format lib",
32
32
  "check:lint": "eslint",
33
33
  "check:format": "prettier --check . && tsp format lib --check",
34
- "checks": "npm run check:lint && npm run check:format"
34
+ "checks": "npm run check:lint && npm run check:format",
35
+ "docs:build": "npx @redocly/cli build-docs tsp-output/@typespec/openapi3/openapi.yaml --output ./dist/redocly.html",
36
+ "docs:preview": "open ./dist/redocly.html",
37
+ "docs": "npm run typespec && npm run docs:build && npm run docs:preview"
35
38
  },
36
39
  "keywords": [
37
40
  "typespec",