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 +149 -0
- package/contracts/content/README.md +108 -0
- package/contracts/content/example.ts +142 -0
- package/contracts/content/heirloom/product.ts +98 -0
- package/contracts/content/index.ts +70 -0
- package/contracts/content/shared/index.ts +13 -0
- package/contracts/content/studioops/artist.ts +102 -0
- package/contracts/content/types.ts +180 -0
- package/contracts/content/utils.ts +148 -0
- package/index.ts +20 -0
- package/package.json +51 -0
- package/sanity/generated/schema-heirloom.ts +223 -0
- package/sanity/generated/schema-studioops.ts +213 -0
- package/sanity/generated/schema.ts +293 -0
- package/sanity/utils.ts +99 -0
- package/tsconfig.json +18 -0
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
|
+
|