Skip to content
Open
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
111 changes: 111 additions & 0 deletions examples/official-site/sqlpage/migrations/08_functions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -478,3 +478,114 @@ VALUES (
'The string to encode.',
'TEXT'
);
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'get_path_segment',
'0.44',
'cut',
'Returns the Nth segment of a path.

### Example

#### Get the user id from the path ''/api/v1/user/42''

```sql
select ''text'' AS component;
select sqlpage.get_path_segment(''/api/v1/user/42'',4) AS contents;
```

#### Result

`42`

#### Notes

- Segments are separated by ''/''.
- If the path is NULL, or the index is out of bounds, it will return an empty string.
- The index is 1-based, so the first segment is at index 1.
'
);
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'get_path_segment',
1,
'path',
'The path to extract the segment from.',
'TEXT'
),
(
'get_path_segment',
2,
'index',
'The index of the segment to extract. 1-based.',
'INTEGER'
);
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'is_path_matching',
'0.44',
'flip-horizontal',
'Returns the path if it matches the pattern, otherwise returns an empty string.

### Example

#### Check if the current path matches a pattern

```sql
select ''text'' AS component;
select sqlpage.is_path_matching(sqlpage.path(),''/api/v1/user/%d/%s'') AS contents;
```

#### Result

`/api/v1/user/42/name`

#### Notes

- The pattern is a list of segments separated by ''/''.
- If the path is NULL, or the pattern is NULL, it will return an empty string.
- If the path and pattern have different numbers of segments, it will return an empty string.
- If the path and pattern have the same number of segments, it will compare them segment by segment.
- If a segment in the pattern is ''%d'', it will match any non-empty segment that is an integer.
- If a segment in the pattern is ''%s'', it will match any non-empty segment in the path.
- If all segments match, it will return the path.
- Otherwise, it will return an empty string.
'
);
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'is_path_matching',
1,
'path',
'The path to match against.',
'TEXT'
),
(
'is_path_matching',
2,
'pattern',
'The template pattern.',
'TEXT'
);
59 changes: 59 additions & 0 deletions src/webserver/database/sqlpage_functions/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ super::function_definition_macro::sqlpage_functions! {
environment_variable(name: Cow<str>);
exec((&RequestInfo), program_name: Cow<str>, args: Vec<Cow<str>>);

get_path_segment(path: Option<Cow<str>>, index: SqlPageFunctionParam<usize>);
is_path_matching(path: Option<Cow<str>>, pattern: Option<Cow<str>>);

fetch((&RequestInfo), http_request: Option<SqlPageFunctionParam<HttpFetchRequest<'_>>>);
fetch_with_meta((&RequestInfo), http_request: Option<SqlPageFunctionParam<HttpFetchRequest<'_>>>);

Expand Down Expand Up @@ -809,6 +812,62 @@ async fn url_encode(raw_text: Option<Cow<'_, str>>) -> Option<Cow<'_, str>> {
})
}

/// Returns the path if it matches the pattern, otherwise returns an empty string.
async fn is_path_matching<'a>(
path: Option<Cow<'a, str>>,
pattern: Option<Cow<'_, str>>,
) -> Option<Cow<'a, str>> {
let (Some(p_val), Some(pattern)) = (path.as_ref(), pattern) else {
return Some(Cow::Borrowed(""));
};

let path_segments: Vec<&str> = p_val.split('/').collect();
let pattern_segments: Vec<&str> = pattern.split('/').collect();

if path_segments.len() != pattern_segments.len() {
return Some(Cow::Borrowed(""));
}

for (ps, pat_s) in path_segments.iter().zip(pattern_segments.iter()) {
if *pat_s == "%s" {
if ps.is_empty() {
return Some(Cow::Borrowed(""));
}
} else if *pat_s == "%d" {
let ps_decoded = percent_encoding::percent_decode_str(ps).decode_utf8_lossy();
if ps_decoded.is_empty() || ps_decoded.parse::<i64>().is_err() {
return Some(Cow::Borrowed(""));
}
} else {
let ps_decoded = percent_encoding::percent_decode_str(ps).decode_utf8_lossy();
let pat_s_decoded = percent_encoding::percent_decode_str(pat_s).decode_utf8_lossy();
if ps_decoded != pat_s_decoded {
return Some(Cow::Borrowed(""));
}
}
}

path
}

/// Returns the Nth segment of a path. Segments are separated by '/'.
async fn get_path_segment(path: Option<Cow<'_, str>>, index: usize) -> String {
let Some(path) = path else {
return String::new();
};
if index == 0 {
return String::new();
}
let segment = path
.trim_start_matches('/')
.split('/')
.nth(index - 1)
.unwrap_or_default();
percent_encoding::percent_decode_str(segment)
.decode_utf8_lossy()
.into_owned()
}

/// Returns all variables in the request as a JSON object.
async fn variables<'a>(
request: &'a ExecutionContext,
Expand Down
Loading