Skip to content

Instantly share code, notes, and snippets.

@jonshea
Created January 28, 2022 15:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jonshea/c08b623e65ba42b9a1aee1fd97335351 to your computer and use it in GitHub Desktop.
Save jonshea/c08b623e65ba42b9a1aee1fd97335351 to your computer and use it in GitHub Desktop.
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://channel-integration.foursquare.com/schema/2022-01-24.json",
"definitions": {
"unixTime": {
"type": "integer",
"minimum": 1514764800,
"$comment": "A Unix time, in seconds. (The minimum value `1514764800` is UTC 2018-01-01 00:00:00.)"
},
"awsRegion": {
"enum": [
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2",
"af-south-1",
"ap-east-1",
"ap-south-1",
"ap-northeast-3",
"ap-northeast-2",
"ap-southeast-1",
"ap-southeast-2",
"ap-northeast-1",
"ca-central-1",
"eu-central-1",
"eu-west-1",
"eu-west-2",
"eu-south-1",
"eu-west-3",
"eu-north-1",
"me-south-1",
"sa-east-1"
],
"$comment": "An AWS region. https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html#Concepts.RegionsAndAvailabilityZones.Regions"
},
"adxChannel": {
"type": "object",
"if": {
"properties": {
"auto-publishing-enabled": {
"const": false
}
}
},
"then": {
"required": [
"productId"
]
},
"required": [
"awsRegion",
"dataSetId"
],
"properties": {
"awsRegion": {
"$ref": "#/definitions/awsRegion",
"$comment": "The region of the data set. All data sets in a single product must be in the same AWS Region. If you import or export an asset to or from an Amazon S3 bucket that is in an AWS Region different from the data set's, your AWS account is charged for the data transfer costs according to Amazon S3 data transfer pricing policies. Each data platform and delivery team is responsible for following best practices by making sure that their data assets are in the same AWS region as their corresponding data sets."
},
"productId": {
"type": "string",
"$comment": "The unique identifier for an ADX product, which is also this product's EntityId in the AWSMarketplace Catalog. This must be a product that the data set (whose id is `dataSetId`) is associated with."
},
"dataSetId": {
"type": "string",
"$comment": "The unique identifier for an ADX data set."
},
"auto-publishing-enabled": {
"type": "boolean",
"$comment": "Any ADX product created after July 22 2021 has automatic revision publishing enabled by default. If the product(s) connected to the specificed dataSetId have automatic revision publishing enabled then this field can be set to `true` and the `productId` field can be omitted. This is particularly useful for when you need a dataset revision to be available to multiple products."
},
"createRevision": {
"type": "object",
"$comment": "The POST request body configuration when making the [CreateRevision](https://docs.aws.amazon.com/data-exchange/latest/apireference/v1-data-sets-datasetid-revisions.html#CreateRevision) API call.",
"properties": {
"comment": {
"$ref": "#/definitions/adxRevisionComment",
"$comment": "An optional comment about the revision. See https://docs.aws.amazon.com/data-exchange/latest/apireference/v1-data-sets-datasetid-revisions.html#v1-data-sets-datasetid-revisions-prop-createrevisionrequest-comment for details."
},
"tags": {
"$ref": "#/definitions/tags",
"$comment": "An optional set of labels that you can assign to a revision when you create it. These should follow [tag convention definitions](https://docs.google.com/spreadsheets/d/1Wcn2S7lCZOsycJHFTdRqg92QUJwJvMjQXexWlT6hk_o)."
}
}
},
"updateRevision": {
"type": "object",
"$comment": "The PATCH request body configuration when making the [UpdateRevision](https://docs.aws.amazon.com/data-exchange/latest/apireference/v1-data-sets-datasetid-revisions-revisionid.html#UpdateRevision) API call.",
"properties": {
"comment": {
"$ref": "#/definitions/adxRevisionComment",
"$comment": "An optional comment about the revision. See https://docs.aws.amazon.com/data-exchange/latest/apireference/v1-data-sets-datasetid-revisions-revisionid.html#v1-data-sets-datasetid-revisions-revisionid-prop-updaterevisionrequest-comment for details."
}
}
}
}
},
"ttdVisitsChannel": {
"type": "object",
"required": [
"feedId",
"feedName"
],
"properties": {
"feedId": {
"type": "string",
"$comment": "The unique ID corresponding to this feed. e.g. '1157'"
},
"feedName": {
"type": "string",
"$comment": "The name of this feed. e.g. 'DOM3302-TTD-Olay-oh8p4q3-2rbo0qk'"
},
"createdBy": {
"type": "string",
"$comment": "The name of the user that created this feed. This should be included for incremental daily deliveries."
},
"startDate": {
"type": "string",
"format": "date",
"$comment": "The date on which this feed is first delivered. This should be included for incremental daily deliveries."
},
"endDate": {
"type": "string",
"format": "date",
"$comment": "The date on which this feed will last be delivered. This should be included for incremental daily deliveries."
},
"dt": {
"$ref": "#/definitions/dt"
}
}
},
"counter": {
"type": "integer",
"minimum": 0,
"$comment": "A non-negative integer."
},
"s3Uri": {
"type": "string",
"format": "uri",
"pattern": "^s3://",
"$comment": "A URI that identifies a resource on S3."
},
"tags": {
"type": "object",
"$comment": "A tag set. This should follow [tag convention definitions](https://docs.google.com/spreadsheets/d/1Wcn2S7lCZOsycJHFTdRqg92QUJwJvMjQXexWlT6hk_o).",
"additionalProperties": {
"type": "string"
}
},
"adxRevisionComment": {
"type": "string",
"$comment": "An optional comment about the revision when making CreateRevisionRequest or UpdateRevisionRequest. See https://docs.aws.amazon.com/data-exchange/latest/apireference/v1-data-sets-datasetid-revisions.html#v1-data-sets-datasetid-revisions-prop-createrevisionrequest-comment and https://docs.aws.amazon.com/data-exchange/latest/apireference/v1-data-sets-datasetid-revisions-revisionid.html#v1-data-sets-datasetid-revisions-revisionid-prop-updaterevisionrequest-comment for details.",
"minLength": 0,
"maxLength": 16384
},
"onOff": {
"enum": [
"ON",
"OFF"
]
},
"dt": {
"type": "string",
"format": "date",
"$comment": "The processing date corresponding to a specified partition. It is meant to be declared at a partition's `feeds` level, not in the `feed.channel` object."
},
"snowflakeChannel": {
"$comment": "Refer to https://docs.snowflake.com/en/user-guide/spark-connector-use.html# for the usage of its fields.",
"type": "object",
"required": [
"sfURL",
"sfUser",
"sfDatabase",
"sfSchema",
"sfWarehouse",
"dbtable",
"saveMode"
],
"properties": {
"sfURL": {
"$comment": "The hostname for the Snowflake account in `<account_identifier>.snowflakecomputing.com` format.",
"type": "string",
"pattern": "\\.snowflakecomputing.com$"
},
"sfUser": {
"$comment": "Login name for the Snowflake user.",
"type": "string"
},
"sfDatabase": {
"$comment": "The database to use for the session after connecting.",
"type": "string"
},
"sfSchema": {
"$comment": "The schema to use for the session after connecting.",
"type": "string"
},
"sfWarehouse": {
"$comment": "The default virtual warehouse to use for the session after connecting.",
"type": "string"
},
"preactions": {
"$comment": "A semicolon-separated list of SQL commands that are executed before data is transferred between Spark and Snowflake.",
"enum": [
"BEGIN;"
]
},
"postactions": {
"$comment": "A semicolon-separated list of SQL commands that are executed after data is transferred between Spark and Snowflake.",
"enum": [
"COMMIT;"
]
},
"truncate_table": {
"$comment": "This controls whether Snowflake retains the schema of a Snowflake target table when overwriting that table.",
"$ref": "#/definitions/onOff"
},
"usestagingtable": {
"$comment": "This controls whether data loading uses a staging table.",
"$ref": "#/definitions/onOff"
},
"column_mapping": {
"$comment": "The Snowflake Spark connector must map columns from the Spark data frame to the Snowflake table. This can be done based on column names (regardless of order), or based on column order (i.e. the first column in the data frame is mapped to the first column in the table, regardless of column name). The default value is `order`.",
"enum": [
"ORDER",
"NAME"
]
},
"column_mismatch_behavior": {
"$comment": "This parameter applies only when the `column_mapping` parameter is set to `name`. If the column names in the Spark data frame and the Snowflake table do not match, then: If `column_mismatch_behavior` is `error`, then the Spark Connector reports an error; If `column_mismatch_behavior` is `ignore`, then the Spark Connector ignores the error. The default value is `error`.",
"enum": [
"IGNORE",
"ERROR"
]
},
"dbtable": {
"$comment": "The Snowflake table to which partition data is written",
"type": "string"
},
"saveMode": {
"$comment": "This specifies the expected behavior of saving a DataFrame to a Snowflake table: https://spark.apache.org/docs/1.6.0/api/java/org/apache/spark/sql/SaveMode.html.",
"enum": [
"APPEND",
"OVERWRITE"
]
},
"selectExpr": {
"type": "array",
"items": {
"type": "string"
},
"$comment": "Spark DataFrame's selectExpr operation: https://spark.apache.org/docs/1.6.3/api/java/org/apache/spark/sql/DataFrame.html#selectExpr(java.lang.String...). If this is not null, `spark.read().format($format).load($s3PartitionLocation).selectExpr($selectExpr)` will be used to load S3 partition data as Spark dataframe. If it's null, `spark.read().format($format).load($s3PartitionLocation)` will be used instead."
},
"basePath": {
"type": "string",
"$comment": "A data source option used to specify the base path that Spark's partition discovery should start with. Spark can generate columns automatically based on partitioning column values encoded in the path of each partition directory. `spark.read().format($format).option(`basePath`, $basePath).load($s3PartitionLocation)` will be used to load partition data as Spark dataframe if this field is not null. Refer to https://spark.apache.org/docs/latest/sql-data-sources-parquet.html#partition-discovery and https://stackoverflow.com/questions/33650421/reading-dataframe-from-partitioned-parquet-file/33656595#33656595 for detailed explanations."
},
"format": {
"$comment": "Spark data source type used when loading the partition data via `spark.read().format($format)`: https://spark.apache.org/docs/latest/sql-data-sources-load-save-functions.html#manually-specifying-options. This is an optional field. If missing, Snowflake deployer will parse the partition's `dataSchema` to populate this field along with other Spark read options based on the info from https://foursquare.atlassian.net/wiki/spaces/DEL/pages/1454374930/Data+Schemas+at+Foursquare#Named-Schemas.",
"enum": [
"PARQUET"
]
}
}
},
"s3VisitsChannel": {
"type": "object",
"required": [
"visitsProduct",
"feedId",
"feedName",
"s3Bucket",
"s3BucketAwsRegion",
"fileFormat",
"hashingOption"
],
"properties": {
"visitsProduct": {
"enum": [
"SVF"
],
"$comment": "The Visits product supported by this channel type for delivery."
},
"feedId": {
"type": "string",
"$comment": "The unique ID corresponding to this feed. e.g. '1476'"
},
"feedName": {
"type": "string",
"$comment": "The name of this feed. e.g. 'DOM4077-Adform-IKEA-StoreLocations2021'"
},
"createdBy": {
"type": "string",
"$comment": "The name of the user that created this feed. This should be included for incremental daily deliveries."
},
"startDate": {
"type": "string",
"format": "date",
"$comment": "The date on which this feed is first delivered. This should be included for incremental daily deliveries."
},
"endDate": {
"type": "string",
"format": "date",
"$comment": "The date on which this feed will last be delivered. This should be included for incremental daily deliveries."
},
"dt": {
"$ref": "#/definitions/dt"
},
"s3Bucket": {
"$ref": "#/definitions/s3Uri",
"$comment": "The internal/external S3 bucket name this feed is configured to be delivered. e.g. 's3://4sq-partner-disney'"
},
"s3BucketAwsRegion": {
"$ref": "#/definitions/awsRegion",
"$comment": "The AWS region of the `s3Bucket`."
},
"fileFormat": {
"enum": [
"TSV",
"PARQUET"
],
"$comment": "The Visits delivery file format configured for this feed."
},
"hashingOption": {
"enum": [
"LOWERCASE_SHA_256",
"PLAIN_TEXT",
"PERSISTENT_ID",
"NO_IDENTIFIER"
],
"$comment": "The hashing option configured for this feed."
}
}
}
},
"type": "object",
"$comment": "data-manifest schema version 2022-01-24",
"required": [
"metadata",
"index",
"feeds",
"partitions"
],
"properties": {
"metadata": {
"type": "object",
"$comment": "Information about this data-manifest JSON file.",
"required": [
"pushTime",
"tags",
"schema"
],
"properties": {
"pushTime": {
"$ref": "#/definitions/unixTime",
"$comment": "The Unix time that the current data-manifest JSON file became available at its location, defined at the earliest time at which consumers of this data could access it. It is each data platform and delivery team's responsibility to make sure that this is always a Unix time in the past."
},
"tags": {
"allOf": [
{
"$ref": "#/definitions/tags"
},
{
"required": [
"global:team"
],
"properties": {
"global:team": {
"type": "string",
"$comment": "The team that produced this data-manifest. The allowed values should follow https://docs.google.com/spreadsheets/d/1Wcn2S7lCZOsycJHFTdRqg92QUJwJvMjQXexWlT6hk_o/edit#gid=274882822&range=A:A."
},
"pipeline:name": {
"type": "string",
"$comment": "The pipeline that produced this data-manifest. The allowed values should be defined by each team https://docs.google.com/spreadsheets/d/1Wcn2S7lCZOsycJHFTdRqg92QUJwJvMjQXexWlT6hk_o/edit#gid=274882822&range=F26. This field will be populated with `default` value by the sync service if missing."
}
}
}
]
},
"schema": {
"const": "http://channel-integration.foursquare.com/schema/2022-01-24.json",
"$comment": "The schema of this data-manifest JSON file."
}
}
},
"index": {
"type": "object",
"$comment": "Information about where to find other data-manifest JSON files produced by the same team and pipeline.",
"required": [
"next"
],
"properties": {
"next": {
"type": "object",
"$comment": "The next data-manifest JSON file produced by the same team and pipeline.",
"required": [
"url",
"expectedPushTime"
],
"properties": {
"url": {
"$ref": "#/definitions/s3Uri",
"$comment": "The next data-manifest file's location."
},
"expectedPushTime": {
"$ref": "#/definitions/unixTime",
"$comment": "The Unix time that the next data-manifest JSON file will be available at its location."
}
}
}
}
},
"feeds": {
"type": "object",
"$comment": "A collection of all ACTIVE and FINALIZED feeds belonging to this team and pipeline.",
"additionalProperties": {
"type": "object",
"$comment": "A feed, representing recurring or ad-hoc definition of the process to extract data for an underlying data set, along with its delivery destination.",
"required": [
"creationTime",
"lifecycle",
"deliveryCadence"
],
"properties": {
"creationTime": {
"$ref": "#/definitions/unixTime",
"$comment": "The Unix time that the current feed was first created."
},
"fifoId": {
"type": "string",
"$comment": "A first-in-first-out identifier for this feed. Currently only a feed of the `SNOWFLAKE` channel type supports this. If set, it's required for this feed that all feeds sharing this fifoId be deployed in a strict sequential order, and each deployment must be successful before the next one begins. Use this ID when you have a mix of REBOOT and INCREMENTAL partitions on separate feeds going to the same destination. A good example of its value can be `deployer_snowflake_fifo_RVF`.",
"pattern": "^deployer_snowflake_fifo_[a-zA-Z0-9_]{3,10}$"
},
"lifecycle": {
"enum": [
"ACTIVE",
"FINALIZED",
"INACTIVE"
],
"$comment": "A feed is ACTIVE if consumers of this feed should expect subsequent deliveries. A feed is FINALIZED for the last delivery that consumers should expect. A feed is INACTIVE when no consumers of this feed need any future deliveries. See https://foursquare.atlassian.net/wiki/spaces/PIM/pages/1785823317/ADX+Technical+Specifications#Feed-Lifecycle for detailed explanations."
},
"deliveryCadence": {
"enum": [
"AD-HOC",
"DAILY",
"WEEKLY",
"BI-WEEKLY",
"MONTHLY",
"OTHER"
],
"$comment": "The delivery schedule of this feed."
},
"channel": {
"type": "object",
"$comment": "The channel destination of this feed.",
"required": [
"type"
],
"properties": {
"type": {
"enum": [
"ADX",
"SNOWFLAKE",
"TTD_VISITS",
"S3_VISITS"
]
}
},
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "ADX"
}
}
},
"then": {
"$ref": "#/definitions/adxChannel"
}
},
{
"if": {
"properties": {
"type": {
"const": "SNOWFLAKE"
}
}
},
"then": {
"$ref": "#/definitions/snowflakeChannel"
}
},
{
"if": {
"properties": {
"type": {
"const": "TTD_VISITS"
}
}
},
"then": {
"$ref": "#/definitions/ttdVisitsChannel"
}
},
{
"if": {
"properties": {
"type": {
"const": "S3_VISITS"
}
}
},
"then": {
"$ref": "#/definitions/s3VisitsChannel"
}
}
]
}
}
}
},
"partitions": {
"type": "array",
"$comment": "New data partitions that have never been reported before for this team and pipeline.",
"uniqueItems": true,
"items": {
"type": "object",
"$comment": "Info about the current partition. A partition is a specific instantiation, or “build,” of a feed, with a specific location.",
"required": [
"location",
"isDir",
"awsRegion",
"awsReaders",
"scope",
"pushTime",
"publicationTime",
"stats",
"dataSchema",
"containsPii",
"feeds"
],
"properties": {
"partitionId": {
"type": "string",
"$comment": "A partition identifier. This should be unique within the current team."
},
"location": {
"$ref": "#/definitions/s3Uri",
"$comment": "The partition location. Only AWS S3 location is supported."
},
"isDir": {
"type": "boolean",
"$comment": "Whether the location is a directory. The data files of a directory partition are the result of listing the directory."
},
"awsRegion": {
"$ref": "#/definitions/awsRegion",
"$comment": "The region of the partition."
},
"awsReaders": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string"
},
"$comment": "A list of IAM ARNs who are permitted to read this partition, and list the contents if it's a directory. This does not need to be an exhaustive list."
},
"scope": {
"enum": [
"INCREMENTAL",
"SCRUB",
"REBOOT"
],
"$comment": "INCREMENTAL indicates that the data within this partition should be considered a diff or an addition to the data already in the data set. SCRUB is a redelivery of a past incremental partition, typically for the purposes of DNSMPI and deletion request compliance. REBOOT is also called an overwrite or a backfill, which indicates that this partition should be considered `complete` for the data set."
},
"pushTime": {
"$ref": "#/definitions/unixTime",
"$comment": "The Unix time that the current partition became available at its location, defined at the earliest time at which consumers of this partition could access it. It is each data platform and delivery team's responsibility to make sure that this is always a Unix time in the past and this partition's pushTime is earlier than the current data-manifest file's pushTime(which means only partitions that have been completely built should be reported in the data-manifest file)."
},
"publicationTime": {
"$ref": "#/definitions/unixTime",
"$comment": "The Unix time that the current partition is `for`, defined as the earliest time for which no new information has flowed into this partition. See https://foursquare.atlassian.net/wiki/spaces/PIM/pages/1785823317/ADX+Technical+Specifications#Publication-time for details."
},
"stats": {
"type": "object",
"$comment": "Stats about the current partition.",
"required": [
"fileCount",
"recordCount",
"totalSizeBytes",
"maxFileSizeBytes"
],
"properties": {
"fileCount": {
"$comment": "The number of files in this partition.",
"allOf": [
{
"$ref": "#/definitions/counter"
},
{
"maximum": 10000,
"$comment": "This is due to ADX's limit: A single revision can contain up to 10,000 assets."
}
]
},
"recordCount": {
"$ref": "#/definitions/counter",
"$comment": "The number of records, also called rows, in the partition."
},
"totalSizeBytes": {
"$ref": "#/definitions/counter",
"$comment": "The total size of the partition, in bytes."
},
"maxFileSizeBytes": {
"$comment": "The maximum individual file size, in bytes. If `isDir` is false, this equals to the `totalSizeBytes`.",
"allOf": [
{
"$ref": "#/definitions/counter"
},
{
"maximum": 10737418240,
"$comment": "This is due to ADX's limit: the maximum size, in GB, of a single asset is 10 GB."
}
]
},
"uniqueDevices": {
"$ref": "#/definitions/counter",
"$comment": "The number of unique devices in a visits partition."
},
"totalVisits": {
"$ref": "#/definitions/counter",
"$comment": "The number of total visits in a visits partition."
}
}
},
"dataSchema": {
"type": [
"string",
"object"
],
"$comment": "Supports string-type [Named Schemas](https://foursquare.atlassian.net/wiki/spaces/DEL/pages/1454374930/Data+Schemas+at+Foursquare#Named-Schemas) and object-type [Anonymous Schemas](https://foursquare.atlassian.net/wiki/spaces/DEL/pages/1454374930/Data+Schemas+at+Foursquare#Anonymous-Schemas-%2F-Declarative-Schemas). The `Named Schema` is a combination of the serialization format and the semantic meaning of the files inside this partition. It should be a description sufficient enough to write a program to read and perform business logic on those data, more than just `TSV` or `Parquet`. An `Anonymous Schema` declares a data schema in terms of its properties, rather than referring to it by a specific name.",
"required": [
"format"
],
"properties": {
"format": {
"enum": [
"TSV"
]
},
"name": {
"type": "string",
"$comment": "An optional naming of this Anonymous Schema."
}
},
"allOf": [
{
"if": {
"properties": {
"format": {
"const": "TSV"
}
}
},
"then": {
"required": [
"containsHeader",
"compression",
"columns"
],
"properties": {
"containsHeader": {
"type": "boolean"
},
"compression": {
"$comment": "compression file extensions/suffixes.",
"enum": [
"NONE",
".tar.gz",
".gz",
".7z",
".bz2",
".lzo"
]
},
"columns": {
"type": "array",
"uniqueItems": true,
"$comment": "The order of items within this `columns` array should follow the order of their appearance in the TSV file.",
"items": {
"type": "object",
"$comment": "Info about a specific column.",
"required": [
"standardName",
"alias",
"isPII"
],
"properties": {
"standardName": {
"type": "string",
"$comment": "The company-wide standard naming of this column. It should be define on the wiki in our list of [Standard Column Names](https://foursquare.atlassian.net/wiki/spaces/DEL/pages/3342499843/Standard+Column+Names+at+Foursquare)"
},
"alias": {
"type": "string",
"$comment": "An alias of this column's `standardName` within this partition's data. It should match what appears in the header if `containsHeader` is true."
},
"isPII": {
"type": "boolean"
}
}
}
}
}
}
}
]
},
"containsPii": {
"type": "boolean",
"$comment": "Whether the partition contains personally-identifiable information."
},
"retentionPolicy": {
"type": "object",
"properties": {
"useByTime": {
"$ref": "#/definitions/unixTime",
"$comment": "The last time that this partition is guaranteed to be present for copying by downstream teams. Past this point, the partition will at some point be deleted or replaced."
},
"sellByTime": {
"$ref": "#/definitions/unixTime",
"$comment": "The last time that this partition can be sold without cross-checking our list of DNSMPI devices."
},
"deleteByTime": {
"$ref": "#/definitions/unixTime",
"$comment": "The last time that this partition should exist in our systems, in order to guarantee compliance with any outstanding deletion requests."
}
}
},
"feeds": {
"type": "object",
"$comment": "All the feeds that the current partition is associated with. The current partition will be delivered to these feeds' channels according to their settings.",
"additionalProperties": {
"type": "object",
"$comment": "This will overwrite/update allowed fields in this feed's `channel` object, mainly to provide customized settings that overwrite the existing configuration when the current partition is delivered to this feed's channel.",
"properties": {
"createRevision": {
"$ref": "#/definitions/adxChannel/properties/createRevision"
},
"updateRevision": {
"$ref": "#/definitions/adxChannel/properties/updateRevision"
},
"basePath": {
"$ref": "#/definitions/snowflakeChannel/properties/basePath"
},
"dt": {
"$ref": "#/definitions/dt"
}
},
"additionalProperties": false
}
}
},
"if": {
"properties": {
"containsPii": {
"const": true
}
}
},
"then": {
"required": [
"retentionPolicy"
],
"properties": {
"retentionPolicy": {
"type": "object",
"required": [
"sellByTime",
"deleteByTime"
]
}
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment