Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/0.1.0 alpha.4 #5

Merged
merged 8 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/breezy-ways-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lens-protocol/metadata': patch
---

**Added** versioning strategy
2 changes: 2 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
"@lens-protocol/metadata": "0.0.1"
},
"changesets": [
"breezy-ways-peel",
"grumpy-hairs-carry",
"grumpy-jokes-drop",
"light-ligers-exist",
"shy-falcons-sip",
"two-pigs-do"
]
}
5 changes: 5 additions & 0 deletions .changeset/shy-falcons-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lens-protocol/metadata': patch
---

**Added** support for token-gated publications
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @lens-protocol/metadata

## 0.1.0-alpha.4

### Patch Changes

- 3f3c349: **Added** versioning strategy
- d643054: **Added** support for token-gated publications

## 0.1.0-alpha.3

### Patch Changes
Expand Down
93 changes: 87 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,16 @@ import {
const publicationMetadata = PublicationMetadataSchema.parse(valid);

switch (publicationMetadata.$schema) {
case PublicationSchemaId.ARTICLE:
case PublicationSchemaId.ARTICLE_LATEST:
// publicationMetadata is ArticleMetadata
break;
case PublicationSchemaId.AUDIO:
case PublicationSchemaId.AUDIO_LATEST:
// publicationMetadata is AudioMetadata
break;
case PublicationSchemaId.IMAGE:
case PublicationSchemaId.IMAGE_LATEST:
// publicationMetadata is ImageMetadata
break;
case PublicationSchemaId.TEXT_ONLY:
case PublicationSchemaId.TEXT_ONLY_LATEST:
// publicationMetadata is TextOnlyMetadata
break;

Expand All @@ -118,7 +118,7 @@ switch (publicationMetadata.$schema) {

**MetadataAttribute**

````typescript
```typescript
import { MetadataAttribute, MetadataAttributeType } from '@lens-protocol/metadata';

switch (attribute.type) {
Expand Down Expand Up @@ -204,7 +204,7 @@ import {
AppId,
Datetime,
} from '@lens-protocol/metadata';
````
```

## JSON schemas

Expand All @@ -220,6 +220,87 @@ import embed from '@lens-protocol/metadata/jsonschemas/profile/1.0.0.json' asser

You can the use them in your JSON Schema validator of choice, for example [ajv](https://ajv.js.org/).

## Versioning

The Lens Protocol Metadata Standards use a **self-describing JSON format**. All metadata files that adopt this standard MUST have a `$schema` property that identifies the schema the file conforms to.

```json
{
"$schema": "https://json-schemas.lens.dev/publications/article/1.0.0.json",

"lens": {
"id": "b3d7f1a0-1f75-11ec-9621-0242ac130002",
"content": "The content of the article",
"locale": "en"
}
}
```

The `$schema` property is a URI that identify the schema type and its version.

**Schemas are versioned using [Semantic Versioning](https://semver.org/)**.

> [!NOTE]
> Even though schemas are identified by URIs, those identifiers are not necessarily network-addressable. They are just identifiers.
> Generally, JSON schema validators don’t make HTTP requests (`https://`) to fetch schemas. Instead, they provide a way to load schemas into an internal schema database. When a schema is referenced by its URI identifier, the schema is retrieved from the internal schema database.

Future changes should aim to be backwards compatible as much as possible.

When adding a new version of a schema, the previous version should be kept for a reasonable amount of time to allow consumers to migrate and to support existing publications.

### Adding a new schema

In this example we will add a new version of the `AudioSchema` schema, but the same process applies to all the other schemas.

- create a new `PublicationSchemaId` enum entry with value of `PublicationSchemaId.AUDIO_LATEST`. Name it after the current schema version (e.g. `AUDIO_V1_0_0`).
- rename the existing `AudioSchema` into `AudioV1_0_0Schema` and update the `$schema` value to `PublicationSchemaId.AUDIO_V1_0_0`
- increase the version number of the `PublicationSchemaId.AUDIO_LATEST` based on the nature of your changes. **Remember to follow semver rules.**
- create a new `AudioSchema` with the new schema definition and use the `PublicationSchemaId.AUDIO_LATEST` as `$schema` value
- update the `scripts/build.ts` script to include the new schema and old schema files under the correct version file name in the `jsonschemas/publications/audio` folder
- release a new version of this package according to the nature of the changes (new major version of a schema = new major version of the package, etc.)

In case the changes are backwards compatible, you could create a single `AudioMetadataDetailsSchema` definition and just declare 2 schemas out of it, one for the old version and one for the new version. For example:

```typescript
export const AudioMetadataDetailsSchema = metadataDetailsWith({
mainContentFocus: mainContentFocus(PublicationMainFocus.AUDIO),

audio: MediaAudioSchema,

attachments: AnyMediaSchema.array()
.min(1)
.optional()
.describe('The other attachments you want to include with it.'),

/** e.g. new optional fields */
});
export type AudioMetadataDetails = z.infer<typeof AudioMetadataDetailsSchema>;

export const AudioSchema = publicationWith({
$schema: z.literal(PublicationSchemaId.AUDIO_LATEST),
lens: AudioMetadataDetailsSchema,
});
export type AudioMetadata = z.infer<typeof AudioSchema>;

export const AudioV1Schema = publicationWith({
$schema: z.literal(PublicationSchemaId.AUDIO_V1_0_0),
lens: AudioMetadataDetailsSchema,
});
export type AudioV1Metadata = z.infer<typeof AudioV1Schema>;
```

In this case consumers of this package can take advantage of the structural likeness and just do the following:

```typescript
switch (publicationMetadata.$schema) {
case PublicationSchemaId.AUDIO_V1_0_0:
case PublicationSchemaId.AUDIO_LATEST:
// publicationMetadata.lens is AudioMetadataDetails
break;
// ...
}
```

## Contributing

To contribute to the Lens Protocol Metadata Standards, please fork this repository and submit a pull request with your changes.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lens-protocol/metadata",
"version": "0.1.0-alpha.3",
"version": "0.1.0-alpha.4",
"description": "Lens Protocol Metadata Standards",
"type": "module",
"main": "./dist/index.cjs",
Expand Down
26 changes: 26 additions & 0 deletions scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ import {
ArticleSchema,
AudioSchema,
CheckingInSchema,
CollectConditionSchema,
EmbedSchema,
EoaOwnershipConditionSchema,
Erc20OwnershipConditionSchema,
EventSchema,
FollowConditionSchema,
GeoLocationSchema,
ImageSchema,
LinkSchema,
Expand All @@ -22,13 +26,22 @@ import {
MetadataAttributeSchema,
MetadataLicenseTypeSchema,
MintSchema,
NetworkAddressSchema,
NftOwnershipConditionSchema,
ProfileMetadataSchema,
ProfileOwnershipConditionSchema,
PublicationEncryptionStrategySchema,
SpaceSchema,
StorySchema,
TextOnlySchema,
ThreeDMetadataSchema,
TransactionSchema,
VideoSchema,
ProfileIdSchema,
EvmAddressSchema,
AccessConditionSchema,
PublicationIdSchema,
AmountSchema,
} from '../src';

const outputDir = 'jsonschemas';
Expand Down Expand Up @@ -63,14 +76,27 @@ for (const [path, Schema] of schemas) {
target: 'jsonSchema7',
definitionPath: '$defs',
definitions: {
AccessCondition: AccessConditionSchema,
Amount: AmountSchema,
AnyMedia: AnyMediaSchema,
CollectCondition: CollectConditionSchema,
EoaOwnershipCondition: EoaOwnershipConditionSchema,
Erc20OwnershipCondition: Erc20OwnershipConditionSchema,
EvmAddress: EvmAddressSchema,
FollowCondition: FollowConditionSchema,
GeoLocation: GeoLocationSchema,
MarketplaceMetadataAttribute: MarketplaceMetadataAttributeSchema,
MediaAudio: MediaAudioSchema,
MediaImage: MediaImageSchema,
MediaVideo: MediaVideoSchema,
MetadataAttribute: MetadataAttributeSchema,
MetadataLicenseType: MetadataLicenseTypeSchema,
NetworkAddress: NetworkAddressSchema,
NftOwnershipCondition: NftOwnershipConditionSchema,
ProfileId: ProfileIdSchema,
ProfileOwnershipCondition: ProfileOwnershipConditionSchema,
PublicationEncryptionStrategy: PublicationEncryptionStrategySchema,
PublicationId: PublicationIdSchema,
},
});

Expand Down
109 changes: 109 additions & 0 deletions src/primitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export function notEmptyString(description?: string) {
* - `en-GB` English as used in the United Kingdom
*/
export type Locale = Brand<string, 'Locale'>;
/**
* @internal
*/
export const LocaleSchema: z.Schema<Locale, z.ZodTypeDef, string> = z
.string({
description: 'A locale identifier.',
Expand All @@ -33,6 +36,9 @@ export const LocaleSchema: z.Schema<Locale, z.ZodTypeDef, string> = z
* A Lens App identifier.
*/
export type AppId = Brand<string, 'AppId'>;
/**
* @internal
*/
export const AppIdSchema: z.Schema<AppId, z.ZodTypeDef, string> = notEmptyString(
'A Lens App identifier.',
).transform((value) => value as AppId);
Expand All @@ -41,6 +47,9 @@ export const AppIdSchema: z.Schema<AppId, z.ZodTypeDef, string> = notEmptyString
* A cryptographic signature.
*/
export type Signature = Brand<string, 'Signature'>;
/**
* @internal
*/
export const SignatureSchema: z.Schema<Signature, z.ZodTypeDef, string> = notEmptyString(
'A cryptographic signature of the Lens metadata.',
).transform((value) => value as Signature);
Expand All @@ -49,6 +58,9 @@ export const SignatureSchema: z.Schema<Signature, z.ZodTypeDef, string> = notEmp
* A markdown text.
*/
export type Markdown = Brand<string, 'Markdown'>;
/**
* @internal
*/
export function markdown(description: string): z.Schema<Markdown, z.ZodTypeDef, string> {
return notEmptyString(description).transform((value) => value as Markdown);
}
Expand All @@ -60,6 +72,9 @@ export function markdown(description: string): z.Schema<Markdown, z.ZodTypeDef,
* an IPFS URI (e.g. ipfs://Qm...), or an Arweave URI (e.g. ar://Qm...).
*/
export type URI = Brand<string, 'URI'>;
/**
* @internal
*/
export function uri(
description: string = 'A Uniform Resource Identifier. ',
): z.Schema<URI, z.ZodTypeDef, string> {
Expand All @@ -70,6 +85,9 @@ export function uri(
.transform((value) => value as URI);
}

/**
* @internal
*/
export const GeoLocationSchema = z.object({
latitude: z.number({ description: 'Latitude in decimal coordinates (e.g. 41.40338).' }),
longitude: z.number({ description: 'Longitude in decimal coordinates (e.g. 2.17403).' }),
Expand All @@ -83,9 +101,100 @@ export type GeoLocation = z.infer<typeof GeoLocationSchema>;
* An ISO 8601 in the JS simplified format: `YYYY-MM-DDTHH:mm:ss.sssZ`.
*/
export type Datetime = Brand<string, 'Datetime'>;
/**
* @internal
*/
export function datetime(description: string): z.Schema<Datetime, z.ZodTypeDef, string> {
return z
.string({ description })
.datetime()
.transform((value) => value as Datetime);
}

/**
* An EVM compatible address.
*/
export type EvmAddress = Brand<string, 'EvmAddress'>;
/**
* @internal
*/
export const EvmAddressSchema: z.Schema<EvmAddress, z.ZodTypeDef, string> = notEmptyString(
'An EVM compatible address.',
).transform((value) => value as EvmAddress);

/**
* An EVM compatible Chain Id.
*/
export type ChainId = Brand<number, 'ChainId'>;
/**
* @internal
*/
export const ChainIdSchema: z.Schema<ChainId, z.ZodTypeDef, number> = z
.number()
.positive()
.transform((value) => value as ChainId);

/**
* @internal
*/
export const NetworkAddressSchema = z.object(
{
chainId: ChainIdSchema,
address: EvmAddressSchema,
},
{
description: 'An EVM compatible address on a specific chain.',
},
);
/**
* An EVM compatible address on a specific chain.
*/
export type NetworkAddress = z.infer<typeof NetworkAddressSchema>;

/**
* An NFT token identifier.
*/
export type TokenId = Brand<string, 'TokenId'>;
/**
* @internal
*/
export const TokenIdSchema: z.Schema<TokenId, z.ZodTypeDef, string> = notEmptyString().transform(
(value) => value as TokenId,
);

/**
* @internal
*/
export const AmountSchema = z.object(
{
currency: NetworkAddressSchema,
value: notEmptyString('The amount in the smallest unit of the currency (e.g. wei for ETH).'),
},
{
description: 'An amount of a specific currency.',
},
);
/**
* An amount of a specific currency.
*/
export type Amount = z.infer<typeof AmountSchema>;

/**
* A Profile identifier.
*/
export type ProfileId = Brand<string, 'ProfileId'>;
/**
* @internal
*/
export const ProfileIdSchema: z.Schema<ProfileId, z.ZodTypeDef, string> =
notEmptyString().transform((value) => value as ProfileId);

/**
* A publication identifier.
*/
export type PublicationId = Brand<string, 'PublicationId'>;
/**
* @internal
*/
export const PublicationIdSchema: z.Schema<PublicationId, z.ZodTypeDef, string> =
notEmptyString().transform((value) => value as PublicationId);
Loading