Skip to content

Commit 2f219c4

Browse files
Support for JSONCollection columns (limosa-io#91)
1 parent 9fefb3f commit 2f219c4

File tree

17 files changed

+395
-26
lines changed

17 files changed

+395
-26
lines changed

Dockerfile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
FROM php:8.0-alpine
1+
FROM php:8.1-alpine
22

33
RUN apk add --no-cache git jq moreutils
4+
RUN apk add --no-cache $PHPIZE_DEPS postgresql-dev \
5+
&& docker-php-ext-install pdo_pgsql \
6+
&& pecl install xdebug-3.1.5 \
7+
&& docker-php-ext-enable xdebug \
8+
&& echo "xdebug.mode=debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
9+
&& echo "xdebug.client_host = 172.19.0.1" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
10+
411
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
512
RUN composer create-project --prefer-dist laravel/laravel example && \
613
cd example

docker-compose.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ services:
33
laravel-scim-server:
44
build: .
55
ports:
6+
# forward xdebug ports
67
- "127.0.0.1:18123:8000"
8+
working_dir: /laravel-scim-server
9+
environment:
10+
- XDEBUG_MODE=debug
11+
- XDEBUG_SESSION=1
712
volumes:
813
- .:/laravel-scim-server

src/Attribute/Attribute.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ public function withFilter($filter)
270270
return $this;
271271
}
272272

273-
public function applyComparison(Builder &$query, Path $path, $parentAttribute = null)
273+
public function applyComparison(Builder &$query, Path $path, Path $parentAttribute = null)
274274
{
275275
throw new SCIMException(sprintf('Comparison is not implemented for "%s"', $this->getFullKey()));
276276
}
@@ -290,7 +290,7 @@ public function patch($operation, $value, Model &$object, Path $path = null)
290290
throw new SCIMException(sprintf('Patch is not implemented for "%s"', $this->getFullKey()));
291291
}
292292

293-
public function remove($value, Model &$object, string $path = null)
293+
public function remove($value, Model &$object, Path $path = null)
294294
{
295295
throw new SCIMException(sprintf('Remove is not implemented for "%s"', $this->getFullKey()));
296296
}

src/Attribute/Collection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ protected function doRead(&$object, $attributes = [])
4949
return $result;
5050
}
5151

52-
public function applyComparison(Builder &$query, Path $path, $parentAttribute = null)
52+
public function applyComparison(Builder &$query, Path $path, Path $parentAttribute = null)
5353
{
5454
if ($path == null || empty($path->getAttributePathAttributes())) {
5555
throw new SCIMException('No attribute path attributes found. Could not apply comparison in ' . $this->getFullKey());

src/Attribute/Complex.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ public function add($value, Model &$object)
218218
}
219219

220220

221-
public function remove($value, Model &$object, string $path = null)
221+
public function remove($value, Model &$object, Path $path = null)
222222
{
223223
if ($this->mutability == 'readOnly') {
224224
// silently ignore

src/Attribute/JSONCollection.php

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
namespace ArieTimmerman\Laravel\SCIMServer\Attribute;
4+
5+
use ArieTimmerman\Laravel\SCIMServer\Exceptions\SCIMException;
6+
use ArieTimmerman\Laravel\SCIMServer\Parser\Path;
7+
use Illuminate\Database\Eloquent\Builder;
8+
use Illuminate\Database\Eloquent\Model;
9+
use Illuminate\Support\Facades\DB;
10+
11+
class JSONCollection extends MutableCollection
12+
{
13+
public function add($value, Model &$object)
14+
{
15+
foreach ($value as $v) {
16+
if (collect($object->{$this->attribute})->contains(
17+
fn ($item) => collect($item)->diffAssoc($v)->isEmpty()
18+
)) {
19+
throw new SCIMException('Value already exists', 400);
20+
}
21+
}
22+
$object->{$this->attribute} = collect($object->{$this->attribute})->merge($value);
23+
}
24+
25+
public function replace($value, Model &$object, ?Path $path = null)
26+
{
27+
$object->{$this->attribute} = $value;
28+
}
29+
30+
public function doRead(&$object, $attributes = [])
31+
{
32+
return $object->{$this->attribute}?->values()->all();
33+
}
34+
35+
public function remove($value, Model &$object, Path $path = null)
36+
{
37+
if ($path?->getValuePathFilter()?->getComparisonExpression() != null) {
38+
$attributes = $path?->getValuePathFilter()?->getComparisonExpression()?->attributePath?->attributeNames;
39+
$operator = $path?->getValuePathFilter()?->getComparisonExpression()?->operator;
40+
$compareValue = $path?->getValuePathFilter()?->getComparisonExpression()?->compareValue;
41+
42+
if ($value != null) {
43+
throw new SCIMException('Non-null value is currently not supported for remove operation with filter');
44+
}
45+
46+
if (count($attributes) != 1) {
47+
throw new SCIMException('Only one attribute is currently supported for remove operation with filter');
48+
}
49+
50+
$object->{$this->attribute} = collect($object->{$this->attribute})->filter(function ($item) use ($attributes, $operator, $compareValue) {
51+
// check operator eq and ne
52+
if ($operator == 'eq') {
53+
return !(isset($item[$attributes[0]]) && $item[$attributes[0]] == $compareValue);
54+
} elseif ($operator == 'ne') {
55+
return !(!isset($item[$attributes[0]]) || $item[$attributes[0]] != $compareValue);
56+
} else {
57+
throw new SCIMException('Unsupported operator for remove operation with filter');
58+
}
59+
})->values()->all();
60+
} else {
61+
foreach ($value as $v) {
62+
$object->{$this->attribute} = collect($object->{$this->attribute})->filter(function ($item) use ($v) {
63+
return !collect($item)->diffAssoc($v)->isEmpty();
64+
})->values()->all();
65+
}
66+
}
67+
}
68+
69+
public function applyComparison(Builder &$query, Path $path, Path $parentAttribute = null)
70+
{
71+
$fieldName = 'value';
72+
73+
if ($path != null && !empty($path->getAttributePathAttributes())) {
74+
$fieldName = $path->getAttributePathAttributes()[0];
75+
}
76+
77+
$operator = $path->node->operator;
78+
$value = $path->node->compareValue;
79+
80+
$exists = false;
81+
82+
foreach ($this->subAttributes as $subAttribute) {
83+
if ($subAttribute->name == $fieldName) {
84+
$exists = true;
85+
break;
86+
}
87+
}
88+
89+
if (!$exists) {
90+
throw new SCIMException('No attribute found with name ' . $path->getAttributePathAttributes()[0]);
91+
}
92+
93+
// check if engine is postgres
94+
if (DB::getConfig("driver") == 'pgsql') {
95+
$baseQuery = sprintf("EXISTS (
96+
SELECT 1
97+
FROM json_array_elements(%s) elem
98+
WHERE elem ->> '%s' LIKE ?
99+
)", $this->attribute, $fieldName);
100+
} elseif (DB::getConfig("driver") == 'sqlite') {
101+
$baseQuery = sprintf("EXISTS (
102+
SELECT 1
103+
FROM json_each(%s) AS elem
104+
WHERE json_extract(elem.value, '$.%s') LIKE ?
105+
)", $this->attribute, $fieldName);
106+
} else {
107+
throw new SCIMException('Unsupported database engine');
108+
}
109+
110+
switch ($operator) {
111+
case "eq":
112+
$query->whereRaw($baseQuery, [addcslashes($value, '%_')]);
113+
break;
114+
case "ne":
115+
if (DB::getConfig("driver") == 'pgsql') {
116+
$baseQuery = sprintf("EXISTS (
117+
SELECT 1
118+
FROM json_array_elements(%s) elem
119+
WHERE elem ->> '%s' NOT LIKE ?
120+
)", $this->attribute, $fieldName);
121+
} elseif (DB::getConfig("driver") == 'sqlite') {
122+
$baseQuery = sprintf("EXISTS (
123+
SELECT 1
124+
FROM json_each(%s) AS elem
125+
WHERE json_extract(elem.value, '$.%s') NOT LIKE ?
126+
)", $this->attribute, $fieldName);
127+
} else {
128+
throw new SCIMException('Unsupported database engine');
129+
}
130+
$query->whereRaw($baseQuery, [addcslashes($value, '%_')])->orWhereNull($this->attribute);
131+
break;
132+
case "co":
133+
$query->whereRaw($baseQuery, ['%' . addcslashes($value, '%_') . "%"]);
134+
break;
135+
case "sw":
136+
// $query->where($jsonAttribute, 'like', addcslashes($value, '%_') . '%');
137+
$query->whereRaw($baseQuery, [addcslashes($value, '%_') . "%"]);
138+
break;
139+
case "ew":
140+
$query->whereRaw($baseQuery, ['%' . addcslashes($value, '%_')]);
141+
break;
142+
case "pr":
143+
$query->whereNotNull($this->attribute);
144+
break;
145+
case "gt":
146+
case "ge":
147+
case "lt":
148+
case "le":
149+
throw new SCIMException("This operator is not supported for this field: " . $operator);
150+
break;
151+
default:
152+
throw new SCIMException("Unknown operator " . $operator);
153+
}
154+
}
155+
}

src/Attribute/Meta.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace ArieTimmerman\Laravel\SCIMServer\Attribute;
44

55
use ArieTimmerman\Laravel\SCIMServer\Helper;
6+
use ArieTimmerman\Laravel\SCIMServer\Parser\Path;
67
use Illuminate\Database\Eloquent\Model;
78

89
class Meta extends Complex
@@ -41,7 +42,7 @@ protected function doRead(&$object, $attributes = [])
4142
);
4243
}
4344

44-
public function remove($value, Model &$object, ?string $path = null)
45+
public function remove($value, Model &$object, Path $path = null)
4546
{
4647
// ignore
4748
}

src/Attribute/MutableCollection.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function add($value, Model &$object)
3131
$object->load($this->attribute);
3232
}
3333

34-
public function remove($value, Model &$object, string $path = null)
34+
public function remove($value, Model &$object, Path $path = null)
3535
{
3636
$values = collect($value)->pluck('value')->all();
3737

@@ -66,9 +66,9 @@ public function patch($operation, $value, Model &$object, ?Path $path = null)
6666
if ($operation == 'add') {
6767
$this->add($value, $object);
6868
} elseif ($operation == 'remove') {
69-
$this->remove($value, $object);
69+
$this->remove($value, $object, $path);
7070
} elseif ($operation == 'replace') {
71-
$this->replace($value, $object);
71+
$this->replace($value, $object, $path);
7272
} else {
7373
throw new SCIMException('Operation not supported: ' . $operation);
7474
}

src/Attribute/Schema.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class Schema extends Complex
77

88
public function generateSchema()
99
{
10-
return [
10+
$result = [
1111
"schemas" => [
1212
"urn:ietf:params:scim:schemas:core:2.0:Schema"
1313
],
@@ -21,9 +21,14 @@ public function generateSchema()
2121
],
2222
// name is substring after last occurence of :
2323
"name" => substr($this->name, strrpos($this->name, ':') + 1),
24-
"description" => $this->description,
2524
"attributes" => collect($this->subAttributes)->map(fn ($element) => $element->generateSchema())->toArray()
2625
];
26+
27+
if($this->description !== null){
28+
$result['description'] = $this->description;
29+
}
30+
31+
return $result;
2732
}
2833

2934
}

src/Http/Controllers/ResourceController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,12 @@ public function update(Request $request, PolicyDecisionPoint $pdp, ResourceType
158158
foreach ($input['Operations'] as $operation) {
159159
switch (strtolower($operation['op'])) {
160160
case "add":
161-
$resourceType->getMapping()->patch('add', $operation['value'], $resourceObject, ParserParser::parse($operation['path'] ?? null));
161+
$resourceType->getMapping()->patch('add', $operation['value'] ?? null, $resourceObject, ParserParser::parse($operation['path'] ?? null));
162162
break;
163163

164164
case "remove":
165165
if (isset($operation['path'])) {
166-
$resourceType->getMapping()->patch('remove', $operation['value'], $resourceObject, ParserParser::parse($operation['path'] ?? null));
166+
$resourceType->getMapping()->patch('remove', $operation['value'] ?? null, $resourceObject, ParserParser::parse($operation['path'] ?? null));
167167
} else {
168168
throw new SCIMException('You MUST provide a "Path"');
169169
}

0 commit comments

Comments
 (0)