levrops-contracts 1.1.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.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # LevrOps Contracts
2
+
3
+ Source of truth for LevrOps API and event contracts, JSON schemas, and generated SDKs.
4
+
5
+ ## Contents
6
+
7
+ - `openapi/` – OpenAPI specifications for the HTTP API (`levrops.v1.yaml`)
8
+ - `asyncapi/` – AsyncAPI contracts for event/webhook payloads (`events.yaml`)
9
+ - `schemas/` – Shared JSON Schemas for core LevrOps domain objects
10
+ - `contracts/content/` – Content contracts for Sanity schema generation
11
+ - `clients/ts/` – TypeScript SDK generated from the OpenAPI specification
12
+ - `sanity/generated/` – Generated Sanity schema from content contracts
13
+ - `COMPAT.json` – Compatibility matrix between API, events, and SDK releases
14
+ - `VERSION` – Repository release version (mirrors the latest API contract)
15
+
16
+ ## Workflow
17
+
18
+ 1. Update JSON schemas and specs
19
+ - Edit `openapi/levrops.v1.yaml` for HTTP endpoints
20
+ - Edit `asyncapi/events.yaml` for event stream/webhook contracts
21
+ - Keep shared domain objects in `schemas/` aligned with the operational data model
22
+ 2. Validate contracts
23
+ - Run Spectral or Redocly CLI locally: `npx @redocly/cli lint openapi/levrops.v1.yaml`
24
+ - Validate AsyncAPI: `npx @asyncapi/cli validate asyncapi/events.yaml`
25
+ 3. Generate SDKs
26
+ - From `clients/ts`: `npm install` then `npm run build`
27
+ - Publish to npm: `npm publish --access public`
28
+ 4. Bump versions & regenerate artifacts
29
+ - Run `python3 scripts/bump_version.py <new-version>` (or `make release VERSION=<new-version>`) to update
30
+ `openapi/levrops.v1.yaml`, `VERSION`, `COMPAT.json`, and the TypeScript SDK (includes rebuilding docs).
31
+ - Use `--events YYYY-MM-DD` (or `make release VERSION=<new-version> EVENTS=YYYY-MM-DD`) to override the events date recorded in `COMPAT.json`.
32
+
33
+ ## TypeScript SDK
34
+
35
+ The TypeScript SDK lives in `clients/ts` and is generated via
36
+ `openapi-typescript-codegen`. Use the provided scripts:
37
+
38
+ ```sh
39
+ cd clients/ts
40
+ npm install
41
+ npm run build # runs generate + compile
42
+ npm publish # publishes @hewnventures/levrops-sdk
43
+ ```
44
+
45
+ Consumers can instantiate the SDK with:
46
+
47
+ ```ts
48
+ import { createLevropsClient } from '@hewnventures/levrops-sdk';
49
+
50
+ const client = createLevropsClient({
51
+ baseUrl: 'https://api.levrops.com',
52
+ accessToken: process.env.LEVROPS_ACCESS_TOKEN,
53
+ tenant: 'heirloom'
54
+ });
55
+
56
+ const contacts = await client.listContacts();
57
+ ```
58
+
59
+ ## CLI Tool
60
+
61
+ A command-line tool is available for managing contracts:
62
+
63
+ ```bash
64
+ # Install the CLI
65
+ make install-cli
66
+ # or: cd cli && pip install -e .
67
+
68
+ # Use it
69
+ levrops-contracts bump 1.2.0 # Bump version and regenerate
70
+ levrops-contracts sync # Sync contracts across all projects
71
+ levrops-contracts validate # Validate OpenAPI/AsyncAPI specs
72
+ levrops-contracts status # Check versions across projects
73
+ levrops-contracts generate-docs # Regenerate API documentation
74
+ ```
75
+
76
+ See `cli/README.md` for full CLI documentation.
77
+
78
+ ## Sanity Integration
79
+
80
+ Content contracts in `contracts/content/` automatically generate Sanity schema definitions with **multi-tenant/property support**.
81
+
82
+ ### For Contract Maintainers
83
+
84
+ 1. Add or modify content contracts in `contracts/content/`
85
+ - Use `shared/` for contracts available to all tenants
86
+ - Use `heirloom/`, `studioops/` for tenant-specific contracts
87
+ - Add `scope` metadata to filter by tenant/property
88
+ 2. Generate schemas:
89
+ ```bash
90
+ npm run sanity:codegen # All contracts
91
+ npm run sanity:codegen -- --tenant=heirloom # Tenant-specific
92
+ npm run sanity:codegen -- --tenant=heirloom --property=store1 # Property-specific
93
+ ```
94
+ 3. Commit the generated schema files
95
+
96
+ ### For Sanity Studio Repositories
97
+
98
+ **Install the package:**
99
+ ```bash
100
+ npm install levrops-contracts
101
+ ```
102
+
103
+ **Import and use schemas:**
104
+
105
+ Option 1: Import all schemas (use runtime filtering):
106
+ ```typescript
107
+ import { allSchemas } from "levrops-contracts/sanity/generated/schema";
108
+ import { filterSchemas } from "levrops-contracts/sanity/utils";
109
+ import { contentContracts } from "levrops-contracts/contracts/content";
110
+
111
+ const tenant = process.env.SANITY_TENANT || "heirloom";
112
+ const tenantSchemas = filterSchemas(allSchemas, tenant, contentContracts);
113
+
114
+ export default {
115
+ name: "default",
116
+ types: tenantSchemas,
117
+ };
118
+ ```
119
+
120
+ Option 2: Import tenant-specific schema:
121
+ ```typescript
122
+ import { allSchemas } from "levrops-contracts/sanity/generated/schema-heirloom";
123
+
124
+ export default {
125
+ name: "default",
126
+ types: allSchemas,
127
+ };
128
+ ```
129
+
130
+ **CI/CD:**
131
+ - Run `npm run sanity:check` in CI to prevent schema drift
132
+ - Regenerate schemas when contracts update
133
+
134
+ See `docs/sanity-integration.md` for detailed integration instructions.
135
+
136
+ ## Versioning
137
+
138
+ - Use semantic versioning for the repo (`VERSION`) and the TypeScript SDK.
139
+ - Use `levrops-contracts bump <new-version>` (or `python3 scripts/bump_version.py <new-version>`, or `make release VERSION=<new-version>`) to keep `VERSION`,
140
+ `COMPAT.json`, and the OpenAPI spec aligned (the script also bumps `clients/ts` versions and rebuilds).
141
+ - When events change, bump the AsyncAPI `info.version` and note the new target in
142
+ `COMPAT.json` (override via `--events` if needed).
143
+
144
+ ## Roadmap
145
+
146
+ - Add Python SDK generation (e.g., via `openapi-python-client`)
147
+ - Expand coverage to additional endpoints and webhook streams
148
+ - Automate validation and SDK publication in CI
149
+
@@ -0,0 +1,108 @@
1
+ # Content Contracts
2
+
3
+ This directory contains content contracts that define Sanity schema models. LevrOps is the source of truth for content models; Sanity Studio consumes generated schema from these contracts.
4
+
5
+ ## Structure
6
+
7
+ - `types.ts` - TypeScript types for content contracts
8
+ - `example.ts` - Example content contracts (can be removed/replaced)
9
+ - `index.ts` - Registry that exports all content contracts
10
+
11
+ ## Multi-Tenant/Property Scoping
12
+
13
+ Contracts can be scoped to specific tenants or properties:
14
+
15
+ ```typescript
16
+ export const myContentContract: ContentContract = {
17
+ id: "my-content",
18
+ title: "My Content",
19
+ type: "document",
20
+ scope: {
21
+ tenants: ["heirloom", "studioops"], // Only available to these tenants
22
+ properties: ["store1"], // Only available to this property
23
+ requiredFor: {
24
+ tenants: ["heirloom"], // Required for Heirloom tenant
25
+ },
26
+ },
27
+ fields: [
28
+ // ... fields
29
+ ],
30
+ };
31
+ ```
32
+
33
+ If `scope` is omitted, the contract is available to all tenants and properties.
34
+
35
+ ## Organization
36
+
37
+ Contracts are organized by namespace:
38
+ - `shared/` - Contracts available to all tenants/properties
39
+ - `heirloom/` - Contracts specific to Heirloom Supply tenant
40
+ - `studioops/` - Contracts specific to StudioOps tenant
41
+
42
+ ## Adding a Content Contract
43
+
44
+ 1. Create a new file in the appropriate directory (shared, tenant-specific, or root)
45
+ 2. Define your contract using the `ContentContract` type:
46
+
47
+ ```typescript
48
+ import type { ContentContract } from "./types";
49
+
50
+ export const myContentContract: ContentContract = {
51
+ id: "my-content",
52
+ title: "My Content",
53
+ type: "document", // or "object"
54
+ description: "Description of this content type",
55
+ fields: [
56
+ {
57
+ name: "title",
58
+ type: "string",
59
+ title: "Title",
60
+ required: true,
61
+ },
62
+ // ... more fields
63
+ ],
64
+ };
65
+ ```
66
+
67
+ 3. Import and add it to `index.ts`:
68
+
69
+ ```typescript
70
+ import { myContentContract } from "./my-content";
71
+ // or for tenant-specific contracts:
72
+ import { myContentContract } from "./heirloom/my-content";
73
+
74
+ export const contentContracts: ContentContract[] = [
75
+ // ... existing contracts
76
+ myContentContract,
77
+ ];
78
+ ```
79
+
80
+ ## Field Types
81
+
82
+ Supported field types:
83
+ - `string` - Single line text
84
+ - `text` - Multi-line text
85
+ - `number` - Numeric value
86
+ - `boolean` - True/false
87
+ - `date` - Date only
88
+ - `datetime` - Date and time
89
+ - `url` - URL
90
+ - `email` - Email address
91
+ - `slug` - URL slug (requires `source` option)
92
+ - `richText` - Portable text/rich text
93
+ - `image` - Image upload
94
+ - `file` - File upload
95
+ - `array` - Array of items (requires `of` option)
96
+ - `object` - Nested object
97
+ - `reference` - Reference to another document (requires `to` option)
98
+
99
+ ## Generating Sanity Schema
100
+
101
+ After adding or modifying contracts, run:
102
+
103
+ ```bash
104
+ npm run sanity:codegen
105
+ ```
106
+
107
+ This generates `sanity/generated/schema.ts` which can be imported in your Sanity Studio project.
108
+
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Example Content Contracts
3
+ *
4
+ * These are example contracts to demonstrate the structure.
5
+ * Remove or replace these with actual content contracts for your project.
6
+ */
7
+
8
+ import type { ContentContract } from "./types";
9
+
10
+ /**
11
+ * Example: Blog Post document
12
+ */
13
+ export const blogPostContract: ContentContract = {
14
+ id: "blog-post",
15
+ title: "Blog Post",
16
+ type: "document",
17
+ description: "A blog post article",
18
+ fields: [
19
+ {
20
+ name: "title",
21
+ type: "string",
22
+ title: "Title",
23
+ required: true,
24
+ options: {
25
+ maxLength: 200,
26
+ },
27
+ },
28
+ {
29
+ name: "slug",
30
+ type: "slug",
31
+ title: "Slug",
32
+ required: true,
33
+ options: {
34
+ source: "title",
35
+ },
36
+ },
37
+ {
38
+ name: "author",
39
+ type: "reference",
40
+ title: "Author",
41
+ required: true,
42
+ options: {
43
+ to: ["author"],
44
+ },
45
+ },
46
+ {
47
+ name: "publishedAt",
48
+ type: "datetime",
49
+ title: "Published At",
50
+ required: true,
51
+ },
52
+ {
53
+ name: "excerpt",
54
+ type: "text",
55
+ title: "Excerpt",
56
+ description: "Short summary for previews",
57
+ options: {
58
+ maxLength: 300,
59
+ },
60
+ },
61
+ {
62
+ name: "content",
63
+ type: "richText",
64
+ title: "Content",
65
+ required: true,
66
+ options: {
67
+ annotations: ["link", "internalLink"],
68
+ decorators: ["strong", "em", "code"],
69
+ },
70
+ },
71
+ {
72
+ name: "tags",
73
+ type: "array",
74
+ title: "Tags",
75
+ options: {
76
+ of: "string",
77
+ },
78
+ },
79
+ {
80
+ name: "featuredImage",
81
+ type: "image",
82
+ title: "Featured Image",
83
+ },
84
+ ],
85
+ preview: {
86
+ select: {
87
+ title: "title",
88
+ author: "author.name",
89
+ publishedAt: "publishedAt",
90
+ },
91
+ prepare: (selection) => ({
92
+ title: selection.title as string,
93
+ subtitle: `${selection.author as string} • ${new Date(selection.publishedAt as string).toLocaleDateString()}`,
94
+ }),
95
+ },
96
+ };
97
+
98
+ /**
99
+ * Example: Hero Section object (nested/reusable component)
100
+ */
101
+ export const heroSectionContract: ContentContract = {
102
+ id: "hero-section",
103
+ title: "Hero Section",
104
+ type: "object",
105
+ description: "Hero banner section for landing pages",
106
+ fields: [
107
+ {
108
+ name: "headline",
109
+ type: "string",
110
+ title: "Headline",
111
+ required: true,
112
+ options: {
113
+ maxLength: 100,
114
+ },
115
+ },
116
+ {
117
+ name: "subheadline",
118
+ type: "text",
119
+ title: "Subheadline",
120
+ options: {
121
+ maxLength: 200,
122
+ },
123
+ },
124
+ {
125
+ name: "image",
126
+ type: "image",
127
+ title: "Background Image",
128
+ required: true,
129
+ },
130
+ {
131
+ name: "ctaText",
132
+ type: "string",
133
+ title: "CTA Button Text",
134
+ },
135
+ {
136
+ name: "ctaLink",
137
+ type: "url",
138
+ title: "CTA Link",
139
+ },
140
+ ],
141
+ };
142
+
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Heirloom Supply Content Contracts
3
+ *
4
+ * Content contracts specific to Heirloom Supply tenant.
5
+ */
6
+
7
+ import type { ContentContract } from "../types";
8
+
9
+ /**
10
+ * Product document for Heirloom Supply
11
+ */
12
+ export const productContract: ContentContract = {
13
+ id: "product",
14
+ title: "Product",
15
+ type: "document",
16
+ description: "Product for Heirloom Supply catalog",
17
+ scope: {
18
+ tenants: ["heirloom"],
19
+ },
20
+ fields: [
21
+ {
22
+ name: "title",
23
+ type: "string",
24
+ title: "Product Name",
25
+ required: true,
26
+ options: {
27
+ maxLength: 200,
28
+ },
29
+ },
30
+ {
31
+ name: "slug",
32
+ type: "slug",
33
+ title: "Slug",
34
+ required: true,
35
+ options: {
36
+ source: "title",
37
+ },
38
+ },
39
+ {
40
+ name: "description",
41
+ type: "richText",
42
+ title: "Description",
43
+ required: true,
44
+ },
45
+ {
46
+ name: "price",
47
+ type: "number",
48
+ title: "Price",
49
+ required: true,
50
+ options: {
51
+ min: 0,
52
+ },
53
+ },
54
+ {
55
+ name: "images",
56
+ type: "array",
57
+ title: "Product Images",
58
+ required: true,
59
+ options: {
60
+ of: "image",
61
+ minLength: 1,
62
+ },
63
+ },
64
+ {
65
+ name: "category",
66
+ type: "reference",
67
+ title: "Category",
68
+ options: {
69
+ to: ["category"],
70
+ },
71
+ },
72
+ {
73
+ name: "tags",
74
+ type: "array",
75
+ title: "Tags",
76
+ options: {
77
+ of: "string",
78
+ },
79
+ },
80
+ {
81
+ name: "inStock",
82
+ type: "boolean",
83
+ title: "In Stock",
84
+ },
85
+ ],
86
+ preview: {
87
+ select: {
88
+ title: "title",
89
+ price: "price",
90
+ inStock: "inStock",
91
+ },
92
+ prepare: (selection) => ({
93
+ title: selection.title as string,
94
+ subtitle: `$${selection.price as number} ${(selection.inStock as boolean) ? "✓ In Stock" : "✗ Out of Stock"}`,
95
+ }),
96
+ },
97
+ };
98
+
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Content Contracts Registry
3
+ *
4
+ * This file exports all content contracts. Add your project's content contracts
5
+ * here to have them included in the generated Sanity schema.
6
+ *
7
+ * Organization:
8
+ * - `shared/` - Contracts available to all tenants/properties
9
+ * - `heirloom/` - Contracts specific to Heirloom Supply tenant
10
+ * - `studioops/` - Contracts specific to StudioOps tenant
11
+ * - `example.ts` - Example contracts (replace with your actual contracts)
12
+ *
13
+ * For new contracts:
14
+ * 1. Create a contract file in the appropriate directory (tenant-specific or shared)
15
+ * 2. Import and add it to the `contentContracts` array below
16
+ */
17
+
18
+ import type { ContentContract } from "./types";
19
+ import { blogPostContract, heroSectionContract } from "./example";
20
+ import { productContract } from "./heirloom/product";
21
+ import { artistContract } from "./studioops/artist";
22
+
23
+ /**
24
+ * Registry of all content contracts.
25
+ * Add your contracts here to include them in generated Sanity schema.
26
+ *
27
+ * Contracts are organized by tenant scope:
28
+ * - Shared contracts (no scope) - available to all tenants
29
+ * - Tenant-specific contracts - only available to specified tenants
30
+ */
31
+ export const contentContracts: ContentContract[] = [
32
+ // Shared contracts (available to all tenants)
33
+ blogPostContract,
34
+ heroSectionContract,
35
+
36
+ // Heirloom Supply contracts
37
+ productContract,
38
+
39
+ // StudioOps contracts
40
+ artistContract,
41
+
42
+ // Add your project's content contracts here:
43
+ // import { myContentContract } from "./my-content";
44
+ // myContentContract,
45
+ ];
46
+
47
+ /**
48
+ * Get a content contract by ID.
49
+ */
50
+ export function getContentContract(id: string): ContentContract | undefined {
51
+ return contentContracts.find((contract) => contract.id === id);
52
+ }
53
+
54
+ /**
55
+ * Get all document contracts (type === "document").
56
+ */
57
+ export function getDocumentContracts(): ContentContract[] {
58
+ return contentContracts.filter((contract) => contract.type === "document");
59
+ }
60
+
61
+ /**
62
+ * Get all object contracts (type === "object").
63
+ */
64
+ export function getObjectContracts(): ContentContract[] {
65
+ return contentContracts.filter((contract) => contract.type === "object");
66
+ }
67
+
68
+ // Re-export utilities for convenience
69
+ export * from "./utils";
70
+
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Shared Content Contracts
3
+ *
4
+ * Content contracts available to all tenants and properties.
5
+ */
6
+
7
+ import { blogPostContract, heroSectionContract } from "../example";
8
+
9
+ // Mark example contracts as shared (no scope = available to all)
10
+ // In practice, you'd update these in example.ts or create new shared contracts here
11
+
12
+ export { blogPostContract, heroSectionContract };
13
+
@@ -0,0 +1,102 @@
1
+ /**
2
+ * StudioOps Content Contracts
3
+ *
4
+ * Content contracts specific to StudioOps tenant.
5
+ */
6
+
7
+ import type { ContentContract } from "../types";
8
+
9
+ /**
10
+ * Artist profile document for StudioOps
11
+ */
12
+ export const artistContract: ContentContract = {
13
+ id: "artist",
14
+ title: "Artist",
15
+ type: "document",
16
+ description: "Artist profile for StudioOps",
17
+ scope: {
18
+ tenants: ["studioops"],
19
+ },
20
+ fields: [
21
+ {
22
+ name: "name",
23
+ type: "string",
24
+ title: "Artist Name",
25
+ required: true,
26
+ options: {
27
+ maxLength: 200,
28
+ },
29
+ },
30
+ {
31
+ name: "slug",
32
+ type: "slug",
33
+ title: "Slug",
34
+ required: true,
35
+ options: {
36
+ source: "name",
37
+ },
38
+ },
39
+ {
40
+ name: "bio",
41
+ type: "richText",
42
+ title: "Biography",
43
+ description: "Artist biography and background",
44
+ },
45
+ {
46
+ name: "photo",
47
+ type: "image",
48
+ title: "Artist Photo",
49
+ },
50
+ {
51
+ name: "genre",
52
+ type: "string",
53
+ title: "Genre",
54
+ options: {
55
+ options: ["rock", "pop", "jazz", "classical", "electronic", "hip-hop", "country"],
56
+ },
57
+ },
58
+ {
59
+ name: "albums",
60
+ type: "array",
61
+ title: "Albums",
62
+ options: {
63
+ of: "reference",
64
+ to: ["album"],
65
+ },
66
+ },
67
+ {
68
+ name: "socialLinks",
69
+ type: "object",
70
+ title: "Social Links",
71
+ fields: [
72
+ {
73
+ name: "website",
74
+ type: "url",
75
+ title: "Website",
76
+ },
77
+ {
78
+ name: "spotify",
79
+ type: "url",
80
+ title: "Spotify",
81
+ },
82
+ {
83
+ name: "youtube",
84
+ type: "url",
85
+ title: "YouTube",
86
+ },
87
+ ],
88
+ },
89
+ ],
90
+ preview: {
91
+ select: {
92
+ title: "name",
93
+ genre: "genre",
94
+ photo: "photo",
95
+ },
96
+ prepare: (selection) => ({
97
+ title: selection.title as string,
98
+ subtitle: selection.genre as string,
99
+ }),
100
+ },
101
+ };
102
+