Skip to content

Conversation

@kartikaysaxena
Copy link
Contributor

@kartikaysaxena kartikaysaxena commented Jul 21, 2025

This PR adds support for relation and namespace level deprecation in schemas.

  • Relations and namespaces can now be annotated with a @deprecated(...) directive to indicate they are deprecated.
  • When a relationship is written to a deprecated relation or deprecated namespace:
    • A warning or error is generated, depending on the specified deprecation level.
    • In the current implementation, writes fail when an error deprecation is configured.
  • Deprecation is associated with the definition/relation decorated with the deprecation.
  • A flag is to be passed to enable this feature, namely --enable-experimental-deprecation along with use directive.

Example:

use deprecation

@deprecated(warn, "comments")
definition user {}

definition resource {
  relation viewer: user | @deprecated(warn, "documents can no longer be public") user:*

  @deprecated(error, "comment goes here")
  relation admin: user
}

Note

Each relation has a object Deprecation assigned to them containing an enum DeprecationType and optional comments, if unspecified DeprecationType defaults to DEPRECATED_TYPE_UNSPECIFIED

Note

Attempted write with a DELETE operation to a deprecated relation/namespace doesn't fail

(related #2465)

@kartikaysaxena kartikaysaxena requested a review from a team as a code owner July 21, 2025 17:31
@github-actions github-actions bot added area/cli Affects the command line area/schema Affects the Schema Language area/api v1 Affects the v1 API area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools) labels Jul 21, 2025
@codecov
Copy link

codecov bot commented Jul 21, 2025

Codecov Report

❌ Patch coverage is 86.18090% with 55 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.47%. Comparing base (d4ff660) to head (d98c374).

Files with missing lines Patch % Lines
pkg/schema/typesystem.go 74.60% 19 Missing and 12 partials ⚠️
pkg/schemadsl/compiler/translator.go 78.79% 8 Missing and 6 partials ⚠️
pkg/schemadsl/parser/parser.go 75.00% 6 Missing and 3 partials ⚠️
pkg/schemadsl/generator/generator.go 97.37% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2504      +/-   ##
==========================================
+ Coverage   79.46%   79.47%   +0.02%     
==========================================
  Files         456      456              
  Lines       47322    47664     +342     
==========================================
+ Hits        37600    37877     +277     
- Misses       6963     7004      +41     
- Partials     2759     2783      +24     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

err,
codes.Aborted,
spiceerrors.ForReason(
v1.ErrorReason_ERROR_REASON_SCHEMA_TYPE_ERROR,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a new error reason into the API and use it here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just raising that into the api repo, once that gets in, i will switch to that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a TODO here

return err
}
reader := ds.SnapshotReader(headRevision)
_, relDef, err := namespace.ReadNamespaceAndRelation(ctx, resource.ObjectType, update.Relationship.Relation, reader)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather than reading the namespace again, let's do this check in the existing validation block, just as an additional check if enabled

Copy link
Contributor Author

@kartikaysaxena kartikaysaxena Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

migrated the check to the relationship validation block to remove unnecessary lookups, also this now uses the typesystem for the read, also added a test

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a method into the type system to retrieve the deprecation status of a namespace and/or relation and then use that? Its good to have it in a single place

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return err
}

if relDef.Deprecation != nil && relDef.Deprecation.DeprecationType != corev1.DeprecationType_DEPRECATED_TYPE_UNSPECIFIED {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can add deprecation to the type system and just read it from there

@ecordell
Copy link
Contributor

Just thinking about the UX a bit (we don't have to solve it in this PR - just thinking ahead!) it would be nice to be able to do something like:

definition document {
  relation viewer: user

  @deprecated(warn, "documents can no longer be public")
  relation viewer: user:*
}

which would normally fail validation as a duplicated relation name

@josephschorr
Copy link
Member

Just thinking about the UX a bit (we don't have to solve it in this PR - just thinking ahead!) it would be nice to be able to do something like:

definition document {
  relation viewer: user

  @deprecated(warn, "documents can no longer be public")
  relation viewer: user:*
}

which would normally fail validation as a duplicated relation name

Not sure... I kind of prefer:

relation viewer: user | @deprecated(warn, "documents can no longer be public") user:*

@kartikaysaxena kartikaysaxena force-pushed the deprecated-objects-rels branch 2 times, most recently from b5137fc to 1ea5e0f Compare July 21, 2025 19:57
@kartikaysaxena
Copy link
Contributor Author

Just thinking about the UX a bit (we don't have to solve it in this PR - just thinking ahead!) it would be nice to be able to do something like:

definition document {
  relation viewer: user

  @deprecated(warn, "documents can no longer be public")
  relation viewer: user:*
}

which would normally fail validation as a duplicated relation name

Not sure... I kind of prefer:

relation viewer: user | @deprecated(warn, "documents can no longer be public") user:*

this would require extending the support to the type reference, would look into that, or address do that in a follow up

@kartikaysaxena kartikaysaxena force-pushed the deprecated-objects-rels branch from 1ea5e0f to 9ce1eed Compare July 22, 2025 17:11
@josephschorr
Copy link
Member

Just thinking about the UX a bit (we don't have to solve it in this PR - just thinking ahead!) it would be nice to be able to do something like:

definition document {
  relation viewer: user

  @deprecated(warn, "documents can no longer be public")
  relation viewer: user:*
}

which would normally fail validation as a duplicated relation name

Not sure... I kind of prefer:

relation viewer: user | @deprecated(warn, "documents can no longer be public") user:*

this would require extending the support to the type reference, would look into that, or address do that in a follow up

Followup is fine

@kartikaysaxena
Copy link
Contributor Author

kartikaysaxena commented Jul 22, 2025

Just thinking about the UX a bit (we don't have to solve it in this PR - just thinking ahead!) it would be nice to be able to do something like:

definition document {
  relation viewer: user

  @deprecated(warn, "documents can no longer be public")
  relation viewer: user:*
}

which would normally fail validation as a duplicated relation name

Not sure... I kind of prefer:

relation viewer: user | @deprecated(warn, "documents can no longer be public") user:*

this would require extending the support to the type reference, would look into that, or address do that in a follow up

Followup is fine

I added it just a while ago 😅, have added tests too, if anything looks sideways, would revert back to previous commit, or if any other suggestion is taken into consideration regarding the type ref approach for relation deprecation, let me know.

Comment on lines 337 to 339
for _, allowed := range relDef.TypeInformation.AllowedDirectRelations {
// check namespace
if allowed.Namespace != rel.Subject.ObjectType {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this iterates over the allowed relation types for a relation to validate the deprecation type, it now supports relation viewer: user | @deprecated(warn, "documents can no longer be public") user:*, also added tests for it.

Comment on lines +97 to +112
for _, allowed := range rel.TypeInformation.GetAllowedDirectRelations() {
// check namespace match
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could've used ts.GetFullRecursiveSubjectTypesForRelation() but that too would've took O(N) time and would want a *core.AllowedRelation at other steps as it gives the string types only

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah

@kartikaysaxena kartikaysaxena force-pushed the deprecated-objects-rels branch from f20b462 to b00779f Compare July 23, 2025 10:01
Comment on lines 43 to 48
for _, update := range updates {
if update.Operation == tuple.UpdateOperationTouch || update.Operation == tuple.UpdateOperationCreate {
relsToCheck = append(relsToCheck, update.Relationship)
}
}
if err := CheckDeprecationsOnRelationships(ctx, relsToCheck, ts); err != nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a little check here as we would not want an attempted write with DELETE operation to a deprecated relation or namespace to fail

Comment on lines 316 to 317
func checkForDeprecatedRelationsAndObjects(ctx context.Context, rel tuple.Relationship, ts *schema.TypeSystem) error {
// Validate if the resource relation is deprecated
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this now uses the typesystem with relevant functions

@kartikaysaxena kartikaysaxena force-pushed the deprecated-objects-rels branch 3 times, most recently from 7419d6f to 3a5f4d4 Compare July 24, 2025 18:54
switch relDep.DeprecationType {
case core.DeprecationType_DEPRECATED_TYPE_WARNING:
log.Warn().
Str("namespace", rel.Resource.ObjectType).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

namespace -> resource_type

}

// Validate if the resource namespace is deprecated
resDep, ok, err := ts.GetDeprecationForNamespace(ctx, rel.Resource.ObjectType)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetDeprecationForObjectType

return err
}
if ok {
switch subDep.DeprecationType {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this "warning vs error" into a helper method and use here and the other two locations


// check deprecation for allowed relation types
dep, ok, err := ts.GetDeprecationForAllowedRelation(
ctx,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be worth having a method GetDeprecationForRelationship and have it return an error/warning message and what kind it is, if any

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but a single relationship might have multiple deprecations (a warning type deprecation at the resource or relation level, and an error type deprecation at the subject level) and it might be necessary to preserve all of them

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these methods would now be invoked in CheckRelationshipDeprecation, does this sound about right?

}`,
"document:foo#editor@testuser:tom",
core.RelationTupleUpdate_CREATE,
"the relation document#editor is deprecated: deprecated, migrate away",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop the the, its cleaner :) - relation ....

err,
codes.Aborted,
spiceerrors.ForReason(
v1.ErrorReason_ERROR_REASON_SCHEMA_TYPE_ERROR,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a TODO here


func NewDeprecationError(namespace, relation, comments string) DeprecationError {
switch {
case relation == "*":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use the string method in the type system to describe the relation: you need to handle wildcards, expiration and caveats, and all should be done in there

},
{
"deprecated relation same subject type with wildcard",
`use deprecation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add tests for deprecation of, say, a subject type without expiration but the one with expiration is still valid (and vice versa) and caveats as well

Copy link
Contributor Author

@kartikaysaxena kartikaysaxena Jul 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added tests of these scenarios including the log and err messages, example "resource_type user with caveat somecaveat and expiration is deprecated:comments here"

}

// GetValidatedDefinition looks up and returns a definition struct, if it has been validated.
func (ts *TypeSystem) GetDeprecationForNamespace(ctx context.Context, definition string) (*core.Deprecation, bool, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add these methods to the type system tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

sg.markNewScope()

for _, relation := range namespace.Relation {
if relation.Deprecation != nil && relation.Deprecation.DeprecationType != core.DeprecationType_DEPRECATED_TYPE_UNSPECIFIED {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra into a helper method

/**
* deprecation contains the required deprecation for the relation.
*/
Deprecation required_deprecation = 8;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be marked as required. The required above means that a caveat (or expiration) must be specified when using the type as part of a subject, which doesn't apply here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed

@kartikaysaxena kartikaysaxena force-pushed the deprecated-objects-rels branch from 3a5f4d4 to 47cda83 Compare July 27, 2025 17:55
Comment on lines +140 to +150
func (ts *TypeSystem) CheckRelationshipDeprecation(ctx context.Context, relationship tuple.Relationship) error {
// Validate if the resource relation is deprecated
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method is now invoked during validation of the relationship being written, and is responsible for checking any applicable deprecations on the resource, relation, or subject

Comment on lines 80 to 89
func (ts *TypeSystem) GetDeprecationForAllowedRelation(
ctx context.Context,
resourceNamespace string,
resourceRelation string,
subjectNamespace string,
subjectRelation string,
isWildcard bool,
hasCaveat bool,
hasExpiration bool,
) (*core.Deprecation, bool, error) {
Copy link
Contributor Author

@kartikaysaxena kartikaysaxena Jul 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this now checks whether the allowed relation reference has a caveat/expiration or both

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth changing this to take in a traits map

@kartikaysaxena kartikaysaxena force-pushed the deprecated-objects-rels branch 2 times, most recently from bb31fc5 to f0bc6ff Compare July 27, 2025 18:35
@kartikaysaxena
Copy link
Contributor Author

Rebased and updated

}

// check if the relation is deprecated for create or touch operations
var relsToCheck []tuple.Relationship
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

relsToCheck := make([]tuple.Relationship, 0, len(updates))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah would improve perf, done

len(relationship.OptionalCaveat.Context.GetFields()) > 0
}

// CheckDeprecationsOnRelationships checks the provided relationships for any deprecations.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// CheckDeprecationsOnRelationships checks the provided relationships for any deprecations, returning an error if applicable

)
}

func NewDeprecationError(err error) DeprecationError {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

object type, relation, or allowed subject type

)
}

func NewDeprecationError(err error) DeprecationError {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also: can we add a test such that if an object type is deprecated, and you attempt to write with it as a subject, it also fails?

require.True(t, traits.AllowsCaveats)
require.True(t, traits.AllowsExpiration)

_, err = vts.PossibleTraitsForSubject("unknown", "user")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add calls to the deprecation methods in the other tests here, and make sure they all return false/nil/empty

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines 80 to 89
func (ts *TypeSystem) GetDeprecationForAllowedRelation(
ctx context.Context,
resourceNamespace string,
resourceRelation string,
subjectNamespace string,
subjectRelation string,
isWildcard bool,
hasCaveat bool,
hasExpiration bool,
) (*core.Deprecation, bool, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth changing this to take in a traits map

case *core.AllowedRelation_Relation:
if isWildcard || w.Relation != subjectRelation {
continue
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a default here that returns a spiceerrors

if len(relationsAndPermissions) == 0 {
ns := namespace.Namespace(nspath)
var ns *core.NamespaceDefinition
if slices.Contains(tctx.enabledFlags, "deprecation") && slices.Contains(tctx.allowedFlags, "deprecation") && (deprecationForNamespace != &core.Deprecation{}) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move if slices.Contains(tctx.enabledFlags, "deprecation") && slices.Contains(tctx.allowedFlags, "deprecation") && (deprecationForNamespace != &core.Deprecation{}) into a helper method

}

// CheckRelationshipDeprecation performs a check over the deprecated relationship's resource, relation, subject and allowed relation, if any
func (ts *TypeSystem) CheckRelationshipDeprecation(ctx context.Context, relationship tuple.Relationship) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A related call to this might also be useful in Validate() (in this package) if the deprecation is an error state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Validate function is also invoked during WriteSchema, so if we call this in Validate, any attempt to write a schema with this feature containing deprecated resources/relations/subjects would result in an error at schema write time.

@kartikaysaxena
Copy link
Contributor Author

checking on this

@kartikaysaxena kartikaysaxena force-pushed the deprecated-objects-rels branch from 1b3efa7 to 5b6d6ac Compare August 5, 2025 17:42
@josephschorr
Copy link
Member

@kartikaysaxena Sorry, I thought this merged. Can you rebase and resolve conflicts?

@github-actions github-actions bot added area/docs Affects docs or metadata (e.g. README) area/datastore Affects the storage system area/dependencies Affects dependencies area/dispatch Affects dispatching of requests area/api http Affects the HTTP Gateway API labels Sep 20, 2025
@github-actions github-actions bot removed area/docs Affects docs or metadata (e.g. README) area/datastore Affects the storage system area/dependencies Affects dependencies area/dispatch Affects dispatching of requests area/api http Affects the HTTP Gateway API labels Sep 20, 2025
@kartikaysaxena kartikaysaxena force-pushed the deprecated-objects-rels branch 3 times, most recently from d0c7c28 to 3408bfa Compare September 20, 2025 14:14
@josephschorr
Copy link
Member

@kartikaysaxena Checking in on this; I'd like to get it in but it has conflicts

@kartikaysaxena kartikaysaxena force-pushed the deprecated-objects-rels branch 2 times, most recently from 6043dc8 to 36e4276 Compare November 4, 2025 12:16
Signed-off-by: Kartikay <[email protected]>
@kartikaysaxena kartikaysaxena force-pushed the deprecated-objects-rels branch from 36e4276 to d98c374 Compare November 4, 2025 12:24
@kartikaysaxena
Copy link
Contributor Author

Hey @josephschorr sorry was busy at work; I have rebased and resolved the the merge conflicts and did a sanity on the changes. Let me know if anything else is needed, happy to help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api v1 Affects the v1 API area/cli Affects the command line area/schema Affects the Schema Language area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants