@common-grants/core 0.1.0-alpha.10

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 ADDED
@@ -0,0 +1,132 @@
1
+ # CommonGrants core library
2
+
3
+ Code for the CommonGrants core specification library, written in TypeSpec. This library is designed to be imported and extended by individual implementations of the CommonGrants protocol.
4
+
5
+ ## 🚀 Quickstart
6
+
7
+ ### Install the library
8
+
9
+ ```bash
10
+ npm install @common-grants/core
11
+ ```
12
+
13
+ ### Project setup
14
+
15
+ A basic project structure that uses the library might look like this:
16
+
17
+ ```
18
+ .
19
+ ├── models.tsp # Extends @common-grants/core models with custom fields
20
+ ├── routes.tsp # Overrides @common-grants/core routes to use the custom models
21
+ ├── main.tsp # Defines an API service that uses the custom models and routes
22
+ |
23
+ ├── tsp-output/ # Directory that stores the output of `tsp compile`, often .gitignored
24
+ |
25
+ ├── package.json # Manages dependencies, commands, and library metadata
26
+ └── tspconfig.yaml # Manages TypeSpec configuration, including emitters
27
+ ```
28
+
29
+ ### Define custom fields
30
+
31
+ The Opportunity model is templated to support custom fields. First define your custom fields by extending the `CustomField` model:
32
+
33
+ ```typespec
34
+ // models.tsp
35
+
36
+ import "@common-grants/core"; // Import the base specification library
37
+
38
+ // Allows us to use models and fields defined in the core library without
39
+ // prefixing each item with `CommonGrants.Models` or `CommonGrants.Fields`
40
+ using CommonGrants.Models;
41
+ using CommonGrants.Fields;
42
+
43
+ namespace CustomAPI.CustomModels;
44
+
45
+ // Define a custom field
46
+ model Agency extends CustomField {
47
+ name: "Agency";
48
+ type: CustomFieldType.string;
49
+
50
+ @example("Department of Transportation")
51
+ value: string;
52
+
53
+ description: "The agency responsible for this opportunity";
54
+ }
55
+
56
+ // Extend the `OpportunityBase` model to create a new `CustomOpportunity` model
57
+ // that includes the new `Agency` field in its `customFields` property
58
+ model CustomOpportunity extends OpportunityBase {
59
+ customFields: {
60
+ agency: Agency;
61
+ };
62
+ }
63
+ ```
64
+
65
+ ### Override default routes
66
+
67
+ The router interfaces are templated to support your custom models. Override them like this:
68
+
69
+ ```typespec
70
+ // routes.tsp
71
+
72
+ import "@common-grants/core";
73
+ import "./models.tsp"; // Import the custom field and model from above
74
+
75
+ using CommonGrants.Routes;
76
+ using TypeSpec.Http;
77
+
78
+ @tag("Search")
79
+ @route("/common-grants/opportunities")
80
+ namespace CustomAPI.CustomRoutes {
81
+ alias OpportunitiesRouter = Opportunities;
82
+
83
+ // Use the default model for list but custom model for read and search
84
+ op list is OpportunitiesRouter.list;
85
+ op read is OpportunitiesRouter.read<CustomModels.CustomOpportunity>;
86
+ op search is OpportunitiesRouter.search<CustomModels.CustomOpportunity>;
87
+ }
88
+ ```
89
+
90
+ ### Define an API service
91
+
92
+ Next, use these updated routes to define an API service:
93
+
94
+ ```typespec
95
+ // main.tsp
96
+
97
+ import "@typespec/http";
98
+
99
+ import "./routes.tsp"; // Import the routes from above
100
+
101
+ using TypeSpec.Http;
102
+
103
+ /** Description of your API goes here */
104
+ @service({
105
+ title: "Custom API",
106
+ })
107
+ namespace CustomAPI;
108
+ ```
109
+
110
+ ### Generate the OpenAPI spec
111
+
112
+ Generate an OpenAPI specification from your `main.tsp` file using either the CLI:
113
+
114
+ ```bash
115
+ npx tsp compile main.tsp --emit "@typespec/openapi3"
116
+ ```
117
+
118
+ Or specify the emitter in `tspconfig.yaml`:
119
+
120
+ ```yaml
121
+ # tspconfig.yaml
122
+ emitters:
123
+ - "@typespec/openapi3"
124
+ ```
125
+
126
+ Both strategies will generate an OpenAPI specification in the `tsp-output/` directory.
127
+
128
+ ### Further reading
129
+
130
+ - See the [TypeSpec documentation](https://typespec.org/docs/getting-started/overview) for more information on how to use TypeSpec.
131
+ - See the [CommonGrants docs](https://hhs.github.io/simpler-grants-protocol/) to learn more about the CommonGrants protocol.
132
+ - See the [CommonGrants CLI](https://www.npmjs.com/package/@common-grants/cli) for more developer tools related to the CommonGrants protocol.
@@ -0,0 +1,2 @@
1
+ export { $lib } from "./lib.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ // Re-export $lib so the compiler can access it and register your library correctly
2
+ export { $lib } from "./lib.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{
2
+ [code: string]: import("@typespec/compiler").DiagnosticMessages;
3
+ }, Record<string, any>, never>;
4
+ export declare const reportDiagnostic: <C extends string | number, M extends keyof {
5
+ [code: string]: import("@typespec/compiler").DiagnosticMessages;
6
+ }[C]>(program: import("@typespec/compiler").Program, diag: import("@typespec/compiler").DiagnosticReport<{
7
+ [code: string]: import("@typespec/compiler").DiagnosticMessages;
8
+ }, C, M>) => void, createDiagnostic: <C extends string | number, M extends keyof {
9
+ [code: string]: import("@typespec/compiler").DiagnosticMessages;
10
+ }[C]>(diag: import("@typespec/compiler").DiagnosticReport<{
11
+ [code: string]: import("@typespec/compiler").DiagnosticMessages;
12
+ }, C, M>) => import("@typespec/compiler").Diagnostic;
13
+ //# sourceMappingURL=lib.d.ts.map
@@ -0,0 +1,10 @@
1
+ import { createTypeSpecLibrary } from "@typespec/compiler";
2
+ export const $lib = createTypeSpecLibrary({
3
+ name: "@common-grants/core",
4
+ diagnostics: {
5
+ // We'll add diagnostics later if needed
6
+ },
7
+ });
8
+ // Optional but convenient, these are meant to be used locally in your library
9
+ export const { reportDiagnostic, createDiagnostic } = $lib;
10
+ //# sourceMappingURL=lib.js.map
package/lib/api.tsp ADDED
@@ -0,0 +1,32 @@
1
+ // Import Schemas.and Routes to make them available outside the package
2
+ import "./core/index.tsp";
3
+ import "@typespec/http";
4
+ import "@typespec/openapi";
5
+
6
+ using TypeSpec.Http;
7
+ using TypeSpec.OpenAPI;
8
+
9
+ /** The base OpenAPI specification for a CommonGrants API
10
+ *
11
+ * In order for an API to be "compliant" with the CommonGrants protocol,
12
+ * it must implement all of the routes with the "required" tag in this specification.
13
+ */
14
+ @service({
15
+ title: "CommonGrants Base API",
16
+ })
17
+ namespace CommonGrants.API;
18
+
19
+ @tag("Opportunities")
20
+ @route("/common-grants/opportunities")
21
+ namespace Opportunities {
22
+ alias Router = Routes.Opportunities;
23
+
24
+ @tag("required")
25
+ op list is Router.list;
26
+
27
+ @tag("required")
28
+ op read is Router.read;
29
+
30
+ @tag("optional")
31
+ op search is Router.search;
32
+ }
@@ -0,0 +1,85 @@
1
+ namespace CommonGrants.Fields;
2
+
3
+ // ########################################
4
+ // Field types definition
5
+ // ########################################
6
+
7
+ /** The set of JSON schema types supported by a custom field */
8
+ enum CustomFieldType {
9
+ string,
10
+ number,
11
+ boolean,
12
+ object,
13
+ array,
14
+ }
15
+
16
+ // ########################################
17
+ // Model definition
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
+ */
37
+ @example(
38
+ Examples.CustomField.programArea,
39
+ #{ title: "String field for program area" }
40
+ )
41
+ @example(
42
+ Examples.CustomField.eligibilityTypes,
43
+ #{ title: "Array field for eligibility types" }
44
+ )
45
+ @doc("A custom field on a model") // Overrides internal docstrings when emitting OpenAPI
46
+ model CustomField {
47
+ /** Name of the custom field */
48
+ name: string;
49
+
50
+ /** The JSON schema type to use when de-serializing the `value` field */
51
+ type: CustomFieldType;
52
+
53
+ /** Link to the full JSON schema for this custom field */
54
+ schema?: url;
55
+
56
+ /** Value of the custom field */
57
+ value: unknown;
58
+
59
+ /** Description of the custom field's purpose */
60
+ description?: string;
61
+ }
62
+
63
+ // ########################################
64
+ // Model examples
65
+ // ########################################
66
+
67
+ /** Examples of the CustomField model */
68
+ namespace Examples.CustomField {
69
+ /** An example of a string custom field */
70
+ const programArea = #{
71
+ name: "programArea",
72
+ type: CustomFieldType.string,
73
+ value: "Healthcare Innovation",
74
+ description: "Primary focus area of the grant program",
75
+ schema: "https://example.com/program-areas.json",
76
+ };
77
+
78
+ /** An example of an array custom field */
79
+ const eligibilityTypes = #{
80
+ name: "eligibilityType",
81
+ type: CustomFieldType.array,
82
+ value: #["nonprofit", "academic"],
83
+ description: "Types of eligible organizations",
84
+ };
85
+ }
@@ -0,0 +1,47 @@
1
+ import "../types.tsp";
2
+
3
+ namespace CommonGrants.Fields;
4
+
5
+ using CommonGrants.Types;
6
+
7
+ // ########################################
8
+ // Model definition
9
+ // ########################################
10
+
11
+ /** Description of an event that has a date (and possible time) associated with it */
12
+ @example(Examples.Event.deadline, #{ title: "Application deadline with time" })
13
+ @example(Examples.Event.openDate, #{ title: "Opening date without time" })
14
+ model Event {
15
+ /** Human-readable name of the event (e.g., 'Application posted', 'Question deadline') */
16
+ name: string;
17
+
18
+ /** Date of the event in in ISO 8601 format: YYYY-MM-DD */
19
+ date: isoDate;
20
+
21
+ /** Time of the event in ISO 8601 format: HH:MM:SS */
22
+ time?: isoTime;
23
+
24
+ /** Description of what this event represents */
25
+ description?: string;
26
+ }
27
+
28
+ // ########################################
29
+ // Model examples
30
+ // ########################################
31
+
32
+ namespace Examples.Event {
33
+ /** An example of a deadline event with a specific time */
34
+ const deadline = #{
35
+ name: "Application Deadline",
36
+ date: isoDate.fromISO("2024-12-31"),
37
+ time: isoTime.fromISO("17:00:00"),
38
+ description: "Final submission deadline for all grant applications",
39
+ };
40
+
41
+ /** An example of an opening date without a specific time */
42
+ const openDate = #{
43
+ name: "Open Date",
44
+ date: isoDate.fromISO("2024-01-15"),
45
+ description: "Applications begin being accepted",
46
+ };
47
+ }
@@ -0,0 +1,20 @@
1
+ import "./custom-field.tsp";
2
+ import "./metadata.tsp";
3
+ import "./money.tsp";
4
+ import "./event.tsp";
5
+
6
+ /** A standard set of fields, e.g. `money` that can be reused across models
7
+ *
8
+ * @example How to use the `Fields` namespace
9
+ *
10
+ * ```typespec
11
+ * import "@common-grants/core";
12
+ *
13
+ * using CommonGrants; // exposes the Fields namespace
14
+ *
15
+ * model MyModel {
16
+ * amount: Fields.money;
17
+ * }
18
+ * ```
19
+ */
20
+ namespace CommonGrants.Fields;
@@ -0,0 +1,42 @@
1
+ namespace CommonGrants.Fields;
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(Examples.Metadata.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
+ /** Examples of the SystemMetadata model */
37
+ namespace Examples.Metadata {
38
+ const system = #{
39
+ createdAt: utcDateTime.fromISO("2025-01-01T17:01:01"),
40
+ lastModifiedAt: utcDateTime.fromISO("2025-01-02T17:30:00"),
41
+ };
42
+ }
@@ -0,0 +1,43 @@
1
+ import "../types.tsp";
2
+
3
+ namespace CommonGrants.Fields;
4
+
5
+ using CommonGrants.Types;
6
+
7
+ // ########################################
8
+ // Model definition
9
+ // ########################################
10
+
11
+ /** A monetary amount and the currency in which its denominated */
12
+ @example(Examples.Money.usdWithCents, #{ title: "US dollars and cents" })
13
+ @example(
14
+ Examples.Money.euroWithoutCents,
15
+ #{ title: "Euros displayed without cents" }
16
+ )
17
+ @example(
18
+ Examples.Money.usdNegative,
19
+ #{ title: "A negative amount of US dollars and cents" }
20
+ )
21
+ model Money {
22
+ /** The amount of money */
23
+ amount: decimalString;
24
+
25
+ /** The ISO 4217 currency code in which the amount is denominated */
26
+ currency: string;
27
+ }
28
+
29
+ // ########################################
30
+ // Model examples
31
+ // ########################################
32
+
33
+ /** Examples of the Money model */
34
+ namespace Examples.Money {
35
+ /** An example of a positive USD amount with cents */
36
+ const usdWithCents = #{ amount: "10000.50", currency: "USD" };
37
+
38
+ /** An example of a positive EUR amount without cents */
39
+ const euroWithoutCents = #{ amount: "5000", currency: "EUR" };
40
+
41
+ /** An example of a negative USD amount in accounting format */
42
+ const usdNegative = #{ amount: "-50.50", currency: "USD" };
43
+ }
@@ -0,0 +1,82 @@
1
+ namespace CommonGrants.Filters;
2
+
3
+ // ############################################################################
4
+ // Filter operators
5
+ // ############################################################################
6
+
7
+ /** Operators that filter a field based on an exact match to a value */
8
+ enum EquivalenceOperators {
9
+ /** Equal to a value */
10
+ eq,
11
+
12
+ /** Not equal to a value */
13
+ neq,
14
+ }
15
+
16
+ /** Operators that filter a field based on a comparison to a value */
17
+ enum ComparisonOperators {
18
+ /** Greater than a value */
19
+ gt,
20
+
21
+ /** Greater than or equal to a value */
22
+ gte,
23
+
24
+ /** Less than a value */
25
+ lt,
26
+
27
+ /** Less than or equal to a value */
28
+ lte,
29
+ }
30
+
31
+ /** Operators that filter a field based on an array of values */
32
+ enum ArrayOperators {
33
+ /** In an array of values */
34
+ in,
35
+
36
+ /** Not in an array of values */
37
+ not_in,
38
+ }
39
+
40
+ /** Operators that filter a field based on a string value */
41
+ enum StringOperators {
42
+ /** Like */
43
+ like,
44
+
45
+ /** Not like */
46
+ not_like,
47
+ }
48
+
49
+ /** Operators that filter a field based on a range of values */
50
+ enum RangeOperators {
51
+ /** Between a range of values */
52
+ between,
53
+
54
+ /** Outside a range of values */
55
+ outside,
56
+ }
57
+
58
+ enum AllOperators {
59
+ ...EquivalenceOperators,
60
+ ...ComparisonOperators,
61
+ ...ArrayOperators,
62
+ ...RangeOperators,
63
+ ...StringOperators,
64
+ }
65
+
66
+ // ############################################################################
67
+ // Filter model
68
+ // ############################################################################
69
+
70
+ /** A base filter model that can be used to create more specific filter models */
71
+ model DefaultFilter {
72
+ /** The operator to apply to the filter value */
73
+ operator:
74
+ | ComparisonOperators
75
+ | ArrayOperators
76
+ | StringOperators
77
+ | RangeOperators
78
+ | AllOperators;
79
+
80
+ /** The value to use for the filter operation */
81
+ value: unknown;
82
+ }
@@ -0,0 +1,38 @@
1
+ import "./base.tsp";
2
+ import "../types.tsp";
3
+
4
+ namespace CommonGrants.Filters;
5
+
6
+ // ############################################################################
7
+ // Date comparison filter
8
+ // ############################################################################
9
+
10
+ /** Filters by comparing a field to a date value */
11
+ model DateComparisonFilter {
12
+ /** The operator to apply to the filter value */
13
+ operator: ComparisonOperators;
14
+
15
+ /** The value to use for the filter operation */
16
+ @example(Types.isoDate.fromISO("2021-01-01"))
17
+ value: Types.isoDate | utcDateTime | offsetDateTime;
18
+ }
19
+
20
+ // ############################################################################
21
+ // Date range filter
22
+ // ############################################################################
23
+
24
+ /** Filters by comparing a field to a range of date values */
25
+ model DateRangeFilter {
26
+ /** The operator to apply to the filter value */
27
+ operator: RangeOperators;
28
+
29
+ /** The value to use for the filter operation */
30
+ @example(#{
31
+ min: Types.isoDate.fromISO("2021-01-01"),
32
+ max: Types.isoDate.fromISO("2021-01-02"),
33
+ })
34
+ value: {
35
+ min: Types.isoDate | utcDateTime | offsetDateTime;
36
+ max: Types.isoDate | utcDateTime | offsetDateTime;
37
+ };
38
+ }
@@ -0,0 +1,37 @@
1
+ import "./base.tsp";
2
+ import "./date.tsp";
3
+ import "./numeric.tsp";
4
+ import "./money.tsp";
5
+ import "./string.tsp";
6
+
7
+ /** A standard set of filters, e.g. `StringArrayFilter`, that can be reused across models
8
+ *
9
+ * @example How to use the `Filters` namespace
10
+ *
11
+ * ```typespec
12
+ * import "@common-grants/core";
13
+ *
14
+ * using CommonGrants; // exposes the Filters namespace
15
+ *
16
+ * model MyFilters extends Record<Filters.DefaultFilter> {
17
+ * @example(#{
18
+ * operator: Filters.FilterOperators.in,
19
+ * value: #["foo", "bar", "baz"],
20
+ * })
21
+ * stringField: Filters.StringArrayFilter;
22
+ *
23
+ * @example(#{ operator: Filters.FilterOperators.gt, value: 10 })
24
+ * numberField: Filters.NumberComparisonFilter;
25
+ *
26
+ * @example(#{
27
+ * operator: Filters.FilterOperators.between,
28
+ * value: #{
29
+ * min: Types.isoDate.fromISO("2021-01-01"),
30
+ * max: Types.isoDate.fromISO("2021-01-02"),
31
+ * },
32
+ * })
33
+ * dateField: Filters.DateRangeFilter;
34
+ * }
35
+ * ```
36
+ */
37
+ namespace CommonGrants.Filters;
@@ -0,0 +1,38 @@
1
+ import "./base.tsp";
2
+ import "../fields/index.tsp";
3
+
4
+ namespace CommonGrants.Filters;
5
+
6
+ // ############################################################################
7
+ // Money comparison filter
8
+ // ############################################################################
9
+
10
+ /** Filters by comparing a field to a monetary value */
11
+ model MoneyComparisonFilter {
12
+ /** The operator to apply to the filter value */
13
+ operator: ComparisonOperators;
14
+
15
+ /** The value to use for the filter operation */
16
+ @example(#{ amount: "1000", currency: "USD" })
17
+ value: Fields.Money;
18
+ }
19
+
20
+ // ############################################################################
21
+ // Date range filter
22
+ // ############################################################################
23
+
24
+ /** Filters by comparing a field to a range of monetary values */
25
+ model MoneyRangeFilter {
26
+ /** The operator to apply to the filter value */
27
+ operator: RangeOperators;
28
+
29
+ /** The value to use for the filter operation */
30
+ @example(#{
31
+ min: #{ amount: "1000", currency: "USD" },
32
+ max: #{ amount: "10000", currency: "USD" },
33
+ })
34
+ value: {
35
+ min: Fields.Money;
36
+ max: Fields.Money;
37
+ };
38
+ }
@@ -0,0 +1,50 @@
1
+ import "../types.tsp";
2
+ import "./base.tsp";
3
+ import "../fields/index.tsp";
4
+
5
+ namespace CommonGrants.Filters;
6
+
7
+ // ############################################################################
8
+ // Number comparison filter
9
+ // ############################################################################
10
+
11
+ /** Filters by comparing a field to a numeric value */
12
+ model NumberComparisonFilter {
13
+ /** The comparison operator to apply to the filter value */
14
+ operator: ComparisonOperators;
15
+
16
+ /** The value to use for the filter operation */
17
+ @example(1000)
18
+ value: numeric;
19
+ }
20
+
21
+ // ############################################################################
22
+ // Number range filter
23
+ // ############################################################################
24
+
25
+ /** Filters by comparing a field to a numeric range */
26
+ model NumberRangeFilter {
27
+ /** The operator to apply to the filter value */
28
+ operator: RangeOperators;
29
+
30
+ /** The value to use for the filter operation */
31
+ @example(#{ min: 1000, max: 10000 })
32
+ value: {
33
+ min: numeric;
34
+ max: numeric;
35
+ };
36
+ }
37
+
38
+ // ############################################################################
39
+ // Number array filter
40
+ // ############################################################################
41
+
42
+ /** Filters by comparing a field to an array of numeric values */
43
+ model NumberArrayFilter {
44
+ /** The operator to apply to the filter value */
45
+ operator: ArrayOperators;
46
+
47
+ /** The value to use for the filter operation */
48
+ @example(#[1000, 2000, 3000])
49
+ value: Array<numeric>;
50
+ }