diff --git a/docs/json-schema/response.json b/docs/json-schema/response.json index b74cf7f1c..95b3b20c2 100644 --- a/docs/json-schema/response.json +++ b/docs/json-schema/response.json @@ -1632,6 +1632,7 @@ "$id" : { "format" : "uri", "minLength" : 1, + "pattern" : "/json_schema/[A-Za-z0-9_-]+/[A-Za-z0-9_-]+/[0-9]+$", "type" : "string" }, "description" : { @@ -1653,10 +1654,17 @@ "JSONSchemaDescriptions" : { "items" : { "additionalProperties" : false, + "if" : { + "properties" : { + "deactivated" : { + "type" : "string" + } + } + }, "properties" : { "$id" : { "format" : "uri-reference", - "pattern" : "^/json_schema/", + "pattern" : "^/json_schema/[A-Za-z0-9_-]+/[A-Za-z0-9_-]+/[0-9]+$", "readOnly" : true, "title" : "Canonical URI Identifier", "type" : "string" @@ -1689,6 +1697,11 @@ "readOnly" : true, "title" : "ID" }, + "latest" : { + "description" : "true when it is the latest of its type-name series", + "title" : "Latest in Type-Name Series?", + "type" : "boolean" + }, "name" : { "$ref" : "common.json#/$defs/json_pointer_token", "readOnly" : true, @@ -1712,10 +1725,18 @@ "type", "name", "version", + "latest", "created", "created_user", "deactivated" ], + "then" : { + "properties" : { + "latest" : { + "const" : false + } + } + }, "type" : "object" }, "type" : "array", diff --git a/docs/modules/Conch::DB::ResultSet::JSONSchema.md b/docs/modules/Conch::DB::ResultSet::JSONSchema.md index 9495c9ea4..d1ff5c54b 100644 --- a/docs/modules/Conch::DB::ResultSet::JSONSchema.md +++ b/docs/modules/Conch::DB::ResultSet::JSONSchema.md @@ -44,6 +44,16 @@ Chainable resultset that restricts the resultset to the single row that matches the indicated resource. (Does **not** fetch the indicated resource content -- you would need a `->column(...)` for that.) +### with\_latest\_flag + +Chainable resultset that adds the `latest` boolean flag to each result, indicating whether +that row is the latest of its type-name series (that is, whether it can be referenced as +`/json_schema/type/name/latest`). + +The query will be closed off as a subselect (that additional chaining will SELECT FROM), +so it makes a difference whether you add things to the resultset before or after calling this +method. + ## LICENSING Copyright Joyent, Inc. diff --git a/json-schema/response.yaml b/json-schema/response.yaml index a9ac8a0c8..878c42c40 100644 --- a/json-schema/response.yaml +++ b/json-schema/response.yaml @@ -1903,6 +1903,7 @@ $defs: type: string minLength: 1 format: uri + pattern: '/json_schema/[A-Za-z0-9_-]+/[A-Za-z0-9_-]+/[0-9]+$' description: $ref: common.yaml#/$defs/non_empty_string JSONSchemaDescriptions: @@ -1918,6 +1919,7 @@ $defs: - type - name - version + - latest - created - created_user - deactivated @@ -1931,7 +1933,7 @@ $defs: readOnly: true type: string format: uri-reference - pattern: '^/json_schema/' + pattern: '^/json_schema/[A-Za-z0-9_-]+/[A-Za-z0-9_-]+/[0-9]+$' description: title: Description type: string @@ -1947,6 +1949,10 @@ $defs: title: Version readOnly: true $ref: common.yaml#/$defs/positive_integer + latest: + title: Latest in Type-Name Series? + description: true when it is the latest of its type-name series + type: boolean created: title: Created readOnly: true @@ -1960,5 +1966,13 @@ $defs: title: Deactivated type: [ 'null', string ] format: date-time + if: + properties: + deactivated: + type: string + then: + properties: + latest: + const: false # vim: set sts=2 sw=2 et : diff --git a/lib/Conch/Controller/JSONSchema.pm b/lib/Conch/Controller/JSONSchema.pm index f521164b3..a8ff2deee 100644 --- a/lib/Conch/Controller/JSONSchema.pm +++ b/lib/Conch/Controller/JSONSchema.pm @@ -325,10 +325,11 @@ sub get_metadata ($c) { } $rs = $rs + ->with_latest_flag # closes off the resultset as a subquery! ->with_description ->with_created_user ->remove_columns([ 'body' ]) - ->order_by([ qw(json_schema.name version) ]); + ->order_by([ qw(json_schema.name json_schema.version) ]); $c->status(200, [ $rs->all ]); } diff --git a/lib/Conch/DB/Result/JSONSchema.pm b/lib/Conch/DB/Result/JSONSchema.pm index 50c0f4ef0..8d99a94a9 100644 --- a/lib/Conch/DB/Result/JSONSchema.pm +++ b/lib/Conch/DB/Result/JSONSchema.pm @@ -200,6 +200,9 @@ sub TO_JSON ($self) { $data->{'$id'} = '/json_schema/'.join('/', $data->@{qw(type name version)}); $data->{description} = $self->get_column('description') if $self->has_column_loaded('description'); + # Mojo::JSON renders \0, \1 as json booleans + $data->{latest} = \$self->get_column('latest'); + if (my $user_cache = $self->related_resultset('created_user')->get_cache) { $data->{created_user} = +{ map +($_ => $user_cache->[0]->$_), qw(id name email) }; } diff --git a/lib/Conch/DB/ResultSet/JSONSchema.pm b/lib/Conch/DB/ResultSet/JSONSchema.pm index 719f24e74..b0d1b5788 100644 --- a/lib/Conch/DB/ResultSet/JSONSchema.pm +++ b/lib/Conch/DB/ResultSet/JSONSchema.pm @@ -93,6 +93,42 @@ sub resource ($self, $type, $name, $version_or_latest) { return $rs; } +=head2 with_latest_flag + +Chainable resultset that adds the C boolean flag to each result, indicating whether +that row is the latest of its type-name series (that is, whether it can be referenced as +C). + +The query will be closed off as a subselect (that additional chaining will SELECT FROM), +so it makes a difference whether you add things to the resultset before or after calling this +method. + +=cut + +sub with_latest_flag ($self) { + my $me = $self->current_source_alias; + + # "Note that first_value, last_value, and nth_value consider only the rows within the + # “window frame”, which by default contains the rows from the start of the partition + # through the last peer of the current row." + # therefore we sort in reverse, so latest comes first and is visible to all rows in the + # window. see https://www.postgresql.org/docs/10/functions-window.html + my $rs = $self + ->add_columns([qw(id type name version deactivated)]) # make sure these columns are available + ->search(undef, { + '+select' => [{ + '' => \"first_value($me.id) over (partition by $me.type, $me.name order by $me.deactivated asc nulls first, version desc)", + -as => 'last_row_id', + }], + }) + ->as_subselect_rs; + + # RT#132276: do not select columns that aren't there + $rs = $rs->columns($self->{attrs}{columns}) if exists $self->{attrs}{columns}; + + return $rs->add_columns({ latest => \"$me.id = last_row_id and $me.deactivated is null" }); +} + 1; __END__ diff --git a/t/integration/json_schema-authed.t b/t/integration/json_schema-authed.t index 25c2141e9..0e18537cb 100644 --- a/t/integration/json_schema-authed.t +++ b/t/integration/json_schema-authed.t @@ -314,6 +314,7 @@ $t_ro->get_ok('/json_schema/foo') type => 'foo', name => 'alpha', version => 1, + latest => JSON::PP::true, created => ignore, created_user => { map +($_ => $ro_user->$_), qw(id name email) }, deactivated => undef, @@ -325,6 +326,7 @@ $t_ro->get_ok('/json_schema/foo') type => 'foo', name => 'bar', version => 1, + latest => JSON::PP::false, created => Conch::Time->new($db_rows[0]->{created})->to_string, created_user => { map +($_ => $ro_user->$_), qw(id name email) }, deactivated => undef, @@ -336,11 +338,13 @@ $t_ro->get_ok('/json_schema/foo') type => 'foo', name => 'bar', version => 2, + latest => JSON::PP::true, created => Conch::Time->new($db_rows[1]->{created})->to_string, created_user => { map +($_ => $ro_user->$_), qw(id name email) }, deactivated => undef, }, ]); + my $metadata = $t_ro->tx->res->json; $t_other->get_ok('/json_schema/foo') @@ -372,7 +376,10 @@ $t_other->delete_ok($_) $t_ro->delete_ok('/json_schema/foo/bar/2') ->status_is(204) ->log_debug_is('Deactivated JSON Schema id '.$schema2_id.' (/json_schema/foo/bar/2); latest of this type and name is now /json_schema/foo/bar/1'); + +$metadata->[1]->{latest} = JSON::PP::true; $metadata->[2]->{deactivated} = re(qr/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3,9}Z$/); +$metadata->[2]->{latest} = JSON::PP::false; $t_ro->delete_ok('/json_schema/'.$schema2_id) ->status_is(410); @@ -403,6 +410,7 @@ $t_ro->delete_ok('/json_schema/foo/bar/1') ->status_is(204) ->log_debug_is('Deactivated JSON Schema id '.$schema1_id.' (/json_schema/foo/bar/1); no schemas of this type and name remain'); $metadata->[1]->{deactivated} = re(qr/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3,9}Z$/); +$metadata->[1]->{latest} = JSON::PP::false; $t_ro->get_ok('/json_schema/foo/bar/1') ->status_is(200)