s3tables: support multi-level namespace normalization

This commit is contained in:
Chris Lu
2026-02-09 19:42:31 -08:00
parent 0b80f055c2
commit be26ce74ce
10 changed files with 345 additions and 144 deletions

View File

@@ -154,14 +154,14 @@ templ S3TablesNamespaces(data dash.S3TablesNamespacesData) {
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="createS3TablesNamespaceForm">
<div class="modal-body">
<input type="hidden" name="bucket_arn" value={ data.BucketARN }/>
<div class="mb-3">
<label for="s3tablesNamespaceName" class="form-label">Namespace</label>
<input type="text" class="form-control" id="s3tablesNamespaceName" name="name" placeholder="analytics" required/>
<div class="form-text">Use lowercase letters, numbers, and underscores. Nested namespaces are not supported.</div>
<div class="modal-body">
<input type="hidden" name="bucket_arn" value={ data.BucketARN }/>
<div class="mb-3">
<label for="s3tablesNamespaceName" class="form-label">Namespace</label>
<input type="text" class="form-control" id="s3tablesNamespaceName" name="name" placeholder="analytics" required/>
<div class="form-text">Use lowercase letters, numbers, and underscores. Use dots for nested namespaces (for example, analytics.daily).</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">
@@ -235,41 +235,53 @@ templ S3TablesNamespaces(data dash.S3TablesNamespacesData) {
input.setCustomValidity('');
return '';
}
let message = '';
if (!name) {
message = 'Namespace name is required';
} else if (name.length < 1 || name.length > 255) {
message = 'Namespace name must be between 1 and 255 characters';
} else if (name === '.' || name === '..') {
message = "namespace name cannot be '.' or '..'";
} else if (name.includes('/')) {
message = "namespace name cannot contain '/'";
} else {
const start = name[0];
const end = name[name.length - 1];
const isStartValid = (start >= 'a' && start <= 'z') || (start >= '0' && start <= '9');
const isEndValid = (end >= 'a' && end <= 'z') || (end >= '0' && end <= '9');
if (!isStartValid) {
message = 'Namespace name must start with a letter or digit';
} else if (!isEndValid) {
message = 'Namespace name must end with a letter or digit';
let message = '';
if (!name) {
message = 'Namespace name is required';
} else if (name.length < 1 || name.length > 255) {
message = 'Namespace name must be between 1 and 255 characters';
} else if (name === '.' || name === '..') {
message = "namespace name cannot be '.' or '..'";
} else if (name.includes('/')) {
message = "namespace name cannot contain '/'";
} else {
for (const ch of name) {
const isLower = ch >= 'a' && ch <= 'z';
const isDigit = ch >= '0' && ch <= '9';
if (!(isLower || isDigit || ch === '_')) {
message = "invalid namespace name: only 'a-z', '0-9', and '_' are allowed";
for (const part of name.split('.')) {
if (!part) {
message = 'namespace levels cannot be empty';
break;
}
const start = part[0];
const end = part[part.length - 1];
const isStartValid = (start >= 'a' && start <= 'z') || (start >= '0' && start <= '9');
const isEndValid = (end >= 'a' && end <= 'z') || (end >= '0' && end <= '9');
if (!isStartValid) {
message = 'Namespace name must start with a letter or digit';
break;
}
if (!isEndValid) {
message = 'Namespace name must end with a letter or digit';
break;
}
for (const ch of part) {
const isLower = ch >= 'a' && ch <= 'z';
const isDigit = ch >= '0' && ch <= '9';
if (!(isLower || isDigit || ch === '_')) {
message = "invalid namespace name: only 'a-z', '0-9', and '_' are allowed";
break;
}
}
if (message) {
break;
}
if (part.startsWith('aws')) {
message = "namespace name cannot start with reserved prefix 'aws'";
break;
}
}
if (!message && name.startsWith('aws')) {
message = "namespace name cannot start with reserved prefix 'aws'";
}
}
input.setCustomValidity(message);
return message;
}
input.setCustomValidity(message);
return message;
}
document.addEventListener('DOMContentLoaded', function() {
s3tablesNamespaceDeleteModalInstance = new bootstrap.Modal(document.getElementById('deleteS3TablesNamespaceModal'));