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.
@@ -0,0 +1,213 @@
1
+ import { defineType, defineField, string, text, number, boolean, date, datetime, url, email, slug, image, file, array, object, reference } from "sanity";
2
+
3
+ /**
4
+ * Generated Sanity Schema
5
+ *
6
+ * This file is auto-generated from content contracts in contracts/content/.
7
+ * DO NOT EDIT THIS FILE DIRECTLY.
8
+ *
9
+ * Content contracts for studioops tenant
10
+ *
11
+ * To update the schema:
12
+ * 1. Update or add contracts in contracts/content/
13
+ * 2. Run: npm run sanity:codegen
14
+ * (or: npm run sanity:codegen -- --tenant=<tenant> --property=<property>)
15
+ *
16
+ * Generated: 2025-11-14T19:30:21.496Z
17
+ */
18
+
19
+ // Blog Post
20
+ // A blog post article
21
+ export const BlogPost = defineType({
22
+ name: "blog-post",
23
+ title: "Blog Post",
24
+ type: "document",
25
+ description: "A blog post article",
26
+ fields: [
27
+ defineField({
28
+ name: "title",
29
+ type: string(),
30
+ title: "Title",
31
+ validation: (Rule) => Rule.required().and(Rule.max(200)),
32
+
33
+ }),
34
+ defineField({
35
+ name: "slug",
36
+ type: slug({ source: "title" }),
37
+ title: "Slug",
38
+ validation: (Rule) => Rule.required(),
39
+
40
+ }),
41
+ defineField({
42
+ name: "author",
43
+ type: reference({ to: [{ type: "author" }] }),
44
+ title: "Author",
45
+ validation: (Rule) => Rule.required(),
46
+
47
+ }),
48
+ defineField({
49
+ name: "publishedAt",
50
+ type: datetime(),
51
+ title: "Published At",
52
+ validation: (Rule) => Rule.required(),
53
+
54
+ }),
55
+ defineField({
56
+ name: "excerpt",
57
+ type: text(),
58
+ title: "Excerpt",
59
+ description: "Short summary for previews",
60
+
61
+ }),
62
+ defineField({
63
+ name: "content",
64
+ type: array({
65
+ of: [{ type: "block" }],
66
+ }),
67
+ title: "Content",
68
+ validation: (Rule) => Rule.required(),
69
+
70
+ }),
71
+ defineField({
72
+ name: "tags",
73
+ type: array({
74
+ of: [string()],
75
+ }),
76
+ title: "Tags",
77
+
78
+ }),
79
+ defineField({
80
+ name: "featuredImage",
81
+ type: image(),
82
+ title: "Featured Image",
83
+
84
+ }),
85
+ ],
86
+ preview: {
87
+ select: {
88
+ "title": "title",
89
+ "author": "author.name",
90
+ "publishedAt": "publishedAt"
91
+ },
92
+ // prepare: (selection) => { ... } // Custom prepare function
93
+ },
94
+ });
95
+
96
+ // Hero Section
97
+ // Hero banner section for landing pages
98
+ export const HeroSection = defineType({
99
+ name: "hero-section",
100
+ title: "Hero Section",
101
+ type: "object",
102
+ description: "Hero banner section for landing pages",
103
+ fields: [
104
+ defineField({
105
+ name: "headline",
106
+ type: string(),
107
+ title: "Headline",
108
+ validation: (Rule) => Rule.required().and(Rule.max(100)),
109
+
110
+ }),
111
+ defineField({
112
+ name: "subheadline",
113
+ type: text(),
114
+ title: "Subheadline",
115
+
116
+ }),
117
+ defineField({
118
+ name: "image",
119
+ type: image(),
120
+ title: "Background Image",
121
+ validation: (Rule) => Rule.required(),
122
+
123
+ }),
124
+ defineField({
125
+ name: "ctaText",
126
+ type: string(),
127
+ title: "CTA Button Text",
128
+
129
+ }),
130
+ defineField({
131
+ name: "ctaLink",
132
+ type: url(),
133
+ title: "CTA Link",
134
+
135
+ }),
136
+ ],
137
+ });
138
+
139
+ // Artist
140
+ // Artist profile for StudioOps
141
+ export const Artist = defineType({
142
+ name: "artist",
143
+ title: "Artist",
144
+ type: "document",
145
+ description: "Artist profile for StudioOps",
146
+ fields: [
147
+ defineField({
148
+ name: "name",
149
+ type: string(),
150
+ title: "Artist Name",
151
+ validation: (Rule) => Rule.required().and(Rule.max(200)),
152
+
153
+ }),
154
+ defineField({
155
+ name: "slug",
156
+ type: slug({ source: "name" }),
157
+ title: "Slug",
158
+ validation: (Rule) => Rule.required(),
159
+
160
+ }),
161
+ defineField({
162
+ name: "bio",
163
+ type: array({
164
+ of: [{ type: "block" }],
165
+ }),
166
+ title: "Biography",
167
+ description: "Artist biography and background",
168
+
169
+ }),
170
+ defineField({
171
+ name: "photo",
172
+ type: image(),
173
+ title: "Artist Photo",
174
+
175
+ }),
176
+ defineField({
177
+ name: "genre",
178
+ type: string(),
179
+ title: "Genre",
180
+ validation: (Rule) => Rule.custom((value) => !value || ["rock", "pop", "jazz", "classical", "electronic", "hip-hop", "country"].includes(value) || "Invalid option"),
181
+
182
+ }),
183
+ defineField({
184
+ name: "albums",
185
+ type: array({
186
+ of: [reference({ to: [{ type: "album" }] })],
187
+ }),
188
+ title: "Albums",
189
+
190
+ }),
191
+ defineField({
192
+ name: "socialLinks",
193
+ type: object({ fields: socialLinksFields }),
194
+ title: "Social Links",
195
+
196
+ }),
197
+ ],
198
+ preview: {
199
+ select: {
200
+ "title": "name",
201
+ "genre": "genre",
202
+ "photo": "photo"
203
+ },
204
+ // prepare: (selection) => { ... } // Custom prepare function
205
+ },
206
+ });
207
+
208
+ // Export all schemas
209
+ export const allSchemas = [
210
+ BlogPost,
211
+ HeroSection,
212
+ Artist,
213
+ ];
@@ -0,0 +1,293 @@
1
+ import { defineType, defineField, string, text, number, boolean, date, datetime, url, email, slug, image, file, array, object, reference } from "sanity";
2
+
3
+ /**
4
+ * Generated Sanity Schema
5
+ *
6
+ * This file is auto-generated from content contracts in contracts/content/.
7
+ * DO NOT EDIT THIS FILE DIRECTLY.
8
+ *
9
+ * All content contracts
10
+ *
11
+ * To update the schema:
12
+ * 1. Update or add contracts in contracts/content/
13
+ * 2. Run: npm run sanity:codegen
14
+ * (or: npm run sanity:codegen -- --tenant=<tenant> --property=<property>)
15
+ *
16
+ * Generated: 2025-11-14T19:30:27.357Z
17
+ */
18
+
19
+ // Blog Post
20
+ // A blog post article
21
+ export const BlogPost = defineType({
22
+ name: "blog-post",
23
+ title: "Blog Post",
24
+ type: "document",
25
+ description: "A blog post article",
26
+ fields: [
27
+ defineField({
28
+ name: "title",
29
+ type: string(),
30
+ title: "Title",
31
+ validation: (Rule) => Rule.required().and(Rule.max(200)),
32
+
33
+ }),
34
+ defineField({
35
+ name: "slug",
36
+ type: slug({ source: "title" }),
37
+ title: "Slug",
38
+ validation: (Rule) => Rule.required(),
39
+
40
+ }),
41
+ defineField({
42
+ name: "author",
43
+ type: reference({ to: [{ type: "author" }] }),
44
+ title: "Author",
45
+ validation: (Rule) => Rule.required(),
46
+
47
+ }),
48
+ defineField({
49
+ name: "publishedAt",
50
+ type: datetime(),
51
+ title: "Published At",
52
+ validation: (Rule) => Rule.required(),
53
+
54
+ }),
55
+ defineField({
56
+ name: "excerpt",
57
+ type: text(),
58
+ title: "Excerpt",
59
+ description: "Short summary for previews",
60
+
61
+ }),
62
+ defineField({
63
+ name: "content",
64
+ type: array({
65
+ of: [{ type: "block" }],
66
+ }),
67
+ title: "Content",
68
+ validation: (Rule) => Rule.required(),
69
+
70
+ }),
71
+ defineField({
72
+ name: "tags",
73
+ type: array({
74
+ of: [string()],
75
+ }),
76
+ title: "Tags",
77
+
78
+ }),
79
+ defineField({
80
+ name: "featuredImage",
81
+ type: image(),
82
+ title: "Featured Image",
83
+
84
+ }),
85
+ ],
86
+ preview: {
87
+ select: {
88
+ "title": "title",
89
+ "author": "author.name",
90
+ "publishedAt": "publishedAt"
91
+ },
92
+ // prepare: (selection) => { ... } // Custom prepare function
93
+ },
94
+ });
95
+
96
+ // Hero Section
97
+ // Hero banner section for landing pages
98
+ export const HeroSection = defineType({
99
+ name: "hero-section",
100
+ title: "Hero Section",
101
+ type: "object",
102
+ description: "Hero banner section for landing pages",
103
+ fields: [
104
+ defineField({
105
+ name: "headline",
106
+ type: string(),
107
+ title: "Headline",
108
+ validation: (Rule) => Rule.required().and(Rule.max(100)),
109
+
110
+ }),
111
+ defineField({
112
+ name: "subheadline",
113
+ type: text(),
114
+ title: "Subheadline",
115
+
116
+ }),
117
+ defineField({
118
+ name: "image",
119
+ type: image(),
120
+ title: "Background Image",
121
+ validation: (Rule) => Rule.required(),
122
+
123
+ }),
124
+ defineField({
125
+ name: "ctaText",
126
+ type: string(),
127
+ title: "CTA Button Text",
128
+
129
+ }),
130
+ defineField({
131
+ name: "ctaLink",
132
+ type: url(),
133
+ title: "CTA Link",
134
+
135
+ }),
136
+ ],
137
+ });
138
+
139
+ // Product
140
+ // Product for Heirloom Supply catalog
141
+ export const Product = defineType({
142
+ name: "product",
143
+ title: "Product",
144
+ type: "document",
145
+ description: "Product for Heirloom Supply catalog",
146
+ fields: [
147
+ defineField({
148
+ name: "title",
149
+ type: string(),
150
+ title: "Product Name",
151
+ validation: (Rule) => Rule.required().and(Rule.max(200)),
152
+
153
+ }),
154
+ defineField({
155
+ name: "slug",
156
+ type: slug({ source: "title" }),
157
+ title: "Slug",
158
+ validation: (Rule) => Rule.required(),
159
+
160
+ }),
161
+ defineField({
162
+ name: "description",
163
+ type: array({
164
+ of: [{ type: "block" }],
165
+ }),
166
+ title: "Description",
167
+ validation: (Rule) => Rule.required(),
168
+
169
+ }),
170
+ defineField({
171
+ name: "price",
172
+ type: number(),
173
+ title: "Price",
174
+ validation: (Rule) => Rule.required().and(Rule.min(0)),
175
+
176
+ }),
177
+ defineField({
178
+ name: "images",
179
+ type: array({
180
+ of: [image()],
181
+ validation: (Rule) => Rule.min(1),
182
+ }),
183
+ title: "Product Images",
184
+ validation: (Rule) => Rule.required(),
185
+
186
+ }),
187
+ defineField({
188
+ name: "category",
189
+ type: reference({ to: [{ type: "category" }] }),
190
+ title: "Category",
191
+
192
+ }),
193
+ defineField({
194
+ name: "tags",
195
+ type: array({
196
+ of: [string()],
197
+ }),
198
+ title: "Tags",
199
+
200
+ }),
201
+ defineField({
202
+ name: "inStock",
203
+ type: boolean(),
204
+ title: "In Stock",
205
+
206
+ }),
207
+ ],
208
+ preview: {
209
+ select: {
210
+ "title": "title",
211
+ "price": "price",
212
+ "inStock": "inStock"
213
+ },
214
+ // prepare: (selection) => { ... } // Custom prepare function
215
+ },
216
+ });
217
+
218
+ // Artist
219
+ // Artist profile for StudioOps
220
+ export const Artist = defineType({
221
+ name: "artist",
222
+ title: "Artist",
223
+ type: "document",
224
+ description: "Artist profile for StudioOps",
225
+ fields: [
226
+ defineField({
227
+ name: "name",
228
+ type: string(),
229
+ title: "Artist Name",
230
+ validation: (Rule) => Rule.required().and(Rule.max(200)),
231
+
232
+ }),
233
+ defineField({
234
+ name: "slug",
235
+ type: slug({ source: "name" }),
236
+ title: "Slug",
237
+ validation: (Rule) => Rule.required(),
238
+
239
+ }),
240
+ defineField({
241
+ name: "bio",
242
+ type: array({
243
+ of: [{ type: "block" }],
244
+ }),
245
+ title: "Biography",
246
+ description: "Artist biography and background",
247
+
248
+ }),
249
+ defineField({
250
+ name: "photo",
251
+ type: image(),
252
+ title: "Artist Photo",
253
+
254
+ }),
255
+ defineField({
256
+ name: "genre",
257
+ type: string(),
258
+ title: "Genre",
259
+ validation: (Rule) => Rule.custom((value) => !value || ["rock", "pop", "jazz", "classical", "electronic", "hip-hop", "country"].includes(value) || "Invalid option"),
260
+
261
+ }),
262
+ defineField({
263
+ name: "albums",
264
+ type: array({
265
+ of: [reference({ to: [{ type: "album" }] })],
266
+ }),
267
+ title: "Albums",
268
+
269
+ }),
270
+ defineField({
271
+ name: "socialLinks",
272
+ type: object({ fields: socialLinksFields }),
273
+ title: "Social Links",
274
+
275
+ }),
276
+ ],
277
+ preview: {
278
+ select: {
279
+ "title": "name",
280
+ "genre": "genre",
281
+ "photo": "photo"
282
+ },
283
+ // prepare: (selection) => { ... } // Custom prepare function
284
+ },
285
+ });
286
+
287
+ // Export all schemas
288
+ export const allSchemas = [
289
+ BlogPost,
290
+ HeroSection,
291
+ Product,
292
+ Artist,
293
+ ];
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Sanity Schema Utilities
3
+ *
4
+ * Runtime utilities for filtering Sanity schemas by tenant and property.
5
+ * These utilities work with generated schemas from content contracts.
6
+ */
7
+
8
+ /**
9
+ * Filter schemas by tenant at runtime.
10
+ *
11
+ * @param schemas - Array of Sanity schema definitions (from generated schema)
12
+ * @param tenant - Tenant slug to filter by
13
+ * @param allContracts - All content contracts to check scope
14
+ * @returns Filtered schemas available to the specified tenant
15
+ */
16
+ export function filterSchemasByTenant<T>(
17
+ schemas: T[],
18
+ tenant: string,
19
+ allContracts: Array<{ id: string; scope?: { tenants?: string[] } }>
20
+ ): T[] {
21
+ // Create a map of contract IDs to their tenant scope
22
+ const contractScopeMap = new Map<string, string[] | undefined>();
23
+ allContracts.forEach((contract) => {
24
+ contractScopeMap.set(contract.id, contract.scope?.tenants);
25
+ });
26
+
27
+ // Filter schemas based on tenant scope
28
+ return schemas.filter((schema: any) => {
29
+ const contractId = schema.name || schema.id;
30
+ const allowedTenants = contractScopeMap.get(contractId);
31
+
32
+ // If no scope or empty scope, available to all tenants
33
+ if (!allowedTenants || allowedTenants.length === 0) {
34
+ return true;
35
+ }
36
+
37
+ // Check if tenant is in the allowed list
38
+ return allowedTenants.includes(tenant);
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Filter schemas by property at runtime.
44
+ *
45
+ * @param schemas - Array of Sanity schema definitions (from generated schema)
46
+ * @param property - Property slug to filter by
47
+ * @param allContracts - All content contracts to check scope
48
+ * @returns Filtered schemas available to the specified property
49
+ */
50
+ export function filterSchemasByProperty<T>(
51
+ schemas: T[],
52
+ property: string,
53
+ allContracts: Array<{ id: string; scope?: { properties?: string[] } }>
54
+ ): T[] {
55
+ // Create a map of contract IDs to their property scope
56
+ const contractScopeMap = new Map<string, string[] | undefined>();
57
+ allContracts.forEach((contract) => {
58
+ contractScopeMap.set(contract.id, contract.scope?.properties);
59
+ });
60
+
61
+ // Filter schemas based on property scope
62
+ return schemas.filter((schema: any) => {
63
+ const contractId = schema.name || schema.id;
64
+ const allowedProperties = contractScopeMap.get(contractId);
65
+
66
+ // If no scope or empty scope, available to all properties
67
+ if (!allowedProperties || allowedProperties.length === 0) {
68
+ return true;
69
+ }
70
+
71
+ // Check if property is in the allowed list
72
+ return allowedProperties.includes(property);
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Filter schemas by tenant and property at runtime.
78
+ *
79
+ * @param schemas - Array of Sanity schema definitions (from generated schema)
80
+ * @param tenant - Tenant slug to filter by
81
+ * @param property - Optional property slug to filter by
82
+ * @param allContracts - All content contracts to check scope
83
+ * @returns Filtered schemas available to the specified tenant (and property if provided)
84
+ */
85
+ export function filterSchemas<T>(
86
+ schemas: T[],
87
+ tenant: string,
88
+ allContracts: Array<{ id: string; scope?: { tenants?: string[]; properties?: string[] } }>,
89
+ property?: string
90
+ ): T[] {
91
+ let filtered = filterSchemasByTenant(schemas, tenant, allContracts);
92
+
93
+ if (property) {
94
+ filtered = filterSchemasByProperty(filtered, property, allContracts);
95
+ }
96
+
97
+ return filtered;
98
+ }
99
+
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "types": ["node"],
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "allowSyntheticDefaultImports": true
14
+ },
15
+ "include": ["contracts/**/*", "tools/**/*"],
16
+ "exclude": ["node_modules", "clients/**/*", "cli/**/*"]
17
+ }
18
+