Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
ascott18 committed Jan 7, 2025
1 parent 9517a06 commit d63d649
Show file tree
Hide file tree
Showing 14 changed files with 195 additions and 46 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
- Added a `color` prop to `c-datetime-picker`.
- Added experimental client-side support for System.Text.Json's PreserveReferences reference handling option in server responses. This does not require changes to your JSON settings in Program.cs - instead, it is activated by setting `refResponse` on the `DataSourceParameters` for a request (i.e. the `$params` object on a ViewModel or ListViewModel). This option can significantly reduce response sizes in cases where the same object occurs many times in a response.
- `useBindToQueryString`/`bindToQueryString` supports primitive collections from metadata without needing to specify explicit parsing logic
- Data Sources now support complex type (object, and arrays of objects) parameters
- Object and array data source parameters can now be passed as JSON, allowing for significantly reduced URL size for parameters like collections of numbers.

## Fixes

- `c-select` `open-on-clear` prop once again functions as expected.
- `c-select` now closes when changing focus to other elements on the page
- Multi-line strings now emit correctly into generated metadata (e.g. a multi-line description for a property)
- Validation attributes on data source parameters are enforced correctly

# 5.2.1

Expand Down
4 changes: 2 additions & 2 deletions docs/modeling/model-components/data-sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ list.$load(1);

## Standard Parameters

All methods on `IDataSource<T>` take a parameter that contains all the client-specified parameters for things paging, searching, sorting, and filtering information. Almost all virtual methods on `StandardDataSource` are also passed the relevant set of parameters.
All methods on `IDataSource<T>` take a parameter that contains all the client-specified parameters for things paging, searching, sorting, and filtering information. Almost all virtual methods on `StandardDataSource` are also passed the relevant set of parameters. The parameters are contained in the `IDataSourceParameters` type or one of its derivatives, `IFilterParameters` (adds filtering and search parameters) or `IListParameters` (filters + pagination).


## Custom Parameters

On any data source that you create, you may add additional properties annotated with `[Coalesce]` that will then be exposed as parameters to the client. These property parameters can be primitives (numeric types, strings, enums), dates (DateTime, DateTimeOffset, DateOnly, TimeOnly), and collections of the preceding types.
On any data source that you create, you may add additional properties annotated with `[Coalesce]` that will then be exposed as parameters to the client. These property parameters can be any type supported by Coalesce, including primitives, dates, [Entity Models](/modeling/model-types/entities.md), [External Types](/modeling/model-types/external-types.md), or collections of any of these.

``` c#
[Coalesce]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
using IntelliTect.Coalesce.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using IntelliTect.Coalesce.Api.Controllers;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;

namespace IntelliTect.Coalesce.CodeGeneration.Tests
{
Expand Down Expand Up @@ -136,6 +138,16 @@ public async Task Validation_TopLevelParameter_ValidatesDirectly()
await res.Content.ReadAsStringAsync());
}

[Fact]
public async Task Security_UsesSecurityFromDtos()
{
HttpClient client = GetClient();
var res = await client.GetStringAsync("""/test/test?dataSource=ParameterTestsSource&dataSource.PersonCriterion={"adminOnly":"bob"}""");

// Not in admin role, so `adminOnly` isn't received
Assert.Equal("""{"personCriterion":{"gender":0,"date":"0001-01-01T00:00:00+00:00"}}""", res);
}

private static HttpClient GetClient() => GetClient<TestController>();

private static HttpClient GetClient<T>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ public class PersonCriteria
public PersonCriteria[] SubCriteria { get; set; }
public Person.Genders Gender { get; set; }
public DateTimeOffset Date { get; set; }
[Edit("Admin")]
public string AdminOnly { get; set; }
}

[Coalesce]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,6 @@ public SelectivePropertyComplexObjectValidationStrategy(ICollection<ModelMetadat
this.properties = properties;
}


/// <inheritdoc />
public IEnumerator<ValidationEntry> GetChildren(
ModelMetadata metadata,
Expand All @@ -272,7 +271,10 @@ public IEnumerator<ValidationEntry> GetChildren(
if (model == null) return Enumerable.Empty<ValidationEntry>().GetEnumerator();

return properties
.Select(p => new ValidationEntry(p, ModelNames.CreatePropertyModelName(key, p.BinderModelName ?? p.PropertyName), p.PropertyGetter(model)))
.Select(p => new ValidationEntry(
p,
ModelNames.CreatePropertyModelName(key, p.BinderModelName ?? p.PropertyName),
p.PropertyGetter!(model)))
.GetEnumerator();
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/IntelliTect.Coalesce/TypeDefinition/ClassViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,10 @@ internal IReadOnlyCollection<PropertyViewModel> Properties
public IEnumerable<PropertyViewModel> DataSourceParameters => Properties
.Where(p =>
!p.IsInternalUse && p.HasPublicSetter && p.HasAttribute<CoalesceAttribute>()
// These are the only supported types, for now
&& (p.PureType.IsPrimitive || p.PureType.IsDateOrTime || p.PureType.IsPOCO)
&& p.PureType.TsTypeKind
is not TypeDiscriminator.File
and not TypeDiscriminator.Void
and not TypeDiscriminator.Unknown
);

/// <summary>
Expand Down
23 changes: 8 additions & 15 deletions src/IntelliTect.Coalesce/TypeDefinition/ReflectionRepository.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
using System;
using IntelliTect.Coalesce.Models;
using IntelliTect.Coalesce.TypeUsage;
using IntelliTect.Coalesce.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using System.Reflection;
using Microsoft.CodeAnalysis;
using IntelliTect.Coalesce.DataAnnotations;
using IntelliTect.Coalesce.Utilities;
using System.Collections.Concurrent;
using IntelliTect.Coalesce.TypeUsage;
using IntelliTect.Coalesce.Api;
using System.Threading;
using IntelliTect.Coalesce.Models;
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;

namespace IntelliTect.Coalesce.TypeDefinition
{
Expand Down
49 changes: 35 additions & 14 deletions src/coalesce-vue/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export function parseValue(
): null | Uint8Array | string;
export function parseValue(value: any, meta: ModelValue): null | object;
export function parseValue(value: any, meta: ObjectValue): null | object;
export function parseValue(value: any, meta: ClassType): null | object;
export function parseValue(
value: any,
meta: PrimitiveValue
Expand All @@ -196,7 +197,7 @@ export function parseValue(value: any, meta: UnknownValue): null | unknown;
export function parseValue(value: any[], meta: CollectionValue): Array<any>;
export function parseValue(
value: any,
meta: Value
meta: Value | ClassType
): null | string | number | boolean | object | Date | Array<any> | unknown {
if (value == null) {
return null;
Expand Down Expand Up @@ -240,15 +241,20 @@ export function parseValue(
throw parseError(value, meta);

case "collection":
if (
type === "string" &&
meta.itemType.type != "model" &&
meta.itemType.type != "object"
) {
return value
.split(",")
.filter((v: any) => v)
.map((v: any) => parseValue(v, meta.itemType));
if (type === "string") {
if (value[0] === "[") {
return JSON.parse(value).map((v: any) =>
parseValue(v, meta.itemType)
);
} else if (
meta.itemType.type != "model" &&
meta.itemType.type != "object"
) {
return value
.split(",")
.filter((v: any) => v)
.map((v: any) => parseValue(v, meta.itemType));
}
}

if (type !== "object" || !Array.isArray(value))
Expand All @@ -258,6 +264,13 @@ export function parseValue(

case "model":
case "object":
if (type === "string") {
if (value.length == 0) return {};
if (value[0] === "{") {
return JSON.parse(value);
}
}

if (type !== "object" || Array.isArray(value))
throw parseError(value, meta);

Expand Down Expand Up @@ -319,8 +332,7 @@ class ModelConversionVisitor extends Visitor<any, any[] | null, any | null> {
): null | Model<TMeta> {
if (value == null) return null;

if (typeof value !== "object" || Array.isArray(value))
throw parseError(value, meta);
value = parseValue(value, meta);

// Prevent infinite recursion on circular object graphs.
if (this.objects.has(value))
Expand Down Expand Up @@ -1124,6 +1136,15 @@ export function bindToQueryString<T, TKey extends keyof T & string>(
);
}

function toString(v: any) {
if (typeof v === "object" && v && !(v instanceof Date)) {
return JSON.stringify(v, (key, value) => {
if (value == null) return undefined;
return value;
});
}
return v?.tostring();
}
const newQuery = {
...//@ts-expect-error
(vue.$router[coalescePendingQuery] || vue.$route.query),
Expand All @@ -1134,9 +1155,9 @@ export function bindToQueryString<T, TKey extends keyof T & string>(
? stringify(v)
: // Use metadata to format the value if the obj has any.
metadata?.params?.[key]
? mapToDto(v, metadata.params[key])?.toString()
? toString(mapToDto(v, metadata.params[key]))
: metadata?.props?.[key]
? mapToDto(v, metadata.props[key])?.toString()
? toString(mapToDto(v, metadata.props[key]))
: // TODO: Add $metadata to DataSourceParameters/FilterParameters/ListParameters, and then support that as well.
// Fallback to .tostring()
String(v) ?? undefined,
Expand Down
1 change: 0 additions & 1 deletion src/coalesce-vue/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,6 @@ function buildParams(
return value;
})
);
debugger;
} else if (obj instanceof Array) {
var isScalarArray = obj.every(isScalarFormValue);
if (obj.length == 0) {
Expand Down
4 changes: 2 additions & 2 deletions src/coalesce-vue/test/api-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ describe("$invoke", () => {

const personList = new PersonListViewModel();
personList.$dataSource = new Person.DataSources.ParameterTestsSource({
personCriteria: new PersonCriteria({
personCriterion: new PersonCriteria({
gender: Genders.Female,
name: "Grace",
personIds: [1, 2, 3],
Expand All @@ -466,7 +466,7 @@ describe("$invoke", () => {
await personList.$load();

expect(AxiosClient.getUri(mock.mock.lastCall![0])).toBe(
`/api/Person/list?page=1&pageSize=10&dataSource=ParameterTestsSource&dataSource.personCriteria={"personIds":[1,2,3],"name":"Grace","subCriteria":[{"personIds":[],"name":"Bob Newbie"}],"gender":2}`
`/api/Person/list?page=1&pageSize=10&dataSource=ParameterTestsSource&dataSource.personCriterion={"personIds":[1,2,3],"name":"Grace","subCriteria":[{"personIds":[],"name":"Bob Newbie"}],"gender":2}`
);
});

Expand Down
32 changes: 30 additions & 2 deletions src/coalesce-vue/vue3-tests/model.spec.vue3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { bindToQueryString, VueInstance } from "../src";
import { mount } from "@vue/test-utils";
import { delay } from "../test/test-utils";
import { reactive } from "vue";
import { Person } from "../../test-targets/models.g";
import { Person, PersonCriteria } from "../../test-targets/models.g";

describe("bindToQueryString", () => {
async function runTest(func: (v: VueInstance, router: Router) => void) {
Expand Down Expand Up @@ -115,7 +115,7 @@ describe("bindToQueryString", () => {
bindToQueryString(v, dataSource, "allowedStatuses");
dataSource.allowedStatuses = [1, 2];
await delay(1);
expect(router.currentRoute.value.query.allowedStatuses).toBe("1,2");
expect(router.currentRoute.value.query.allowedStatuses).toBe("[1,2]");

router.push("/?allowedStatuses=2,3");
await delay(1);
Expand All @@ -127,6 +127,34 @@ describe("bindToQueryString", () => {
});
});

test("bound to object", async () => {
const dataSource = reactive(new Person.DataSources.ParameterTestsSource());
await runTest(async (v, router) => {
bindToQueryString(v, dataSource, "personCriterion");
dataSource.personCriterion = new PersonCriteria({
name: "b&personCriterion=ob",
personIds: [1, 2],
});
await delay(1);
expect(router.currentRoute.value.query.personCriterion).toBe(
`{"personIds":[1,2],"name":"b&personCriterion=ob"}`
);

router.push(`/?personCriterion={"name":"bob2","personIds":[1,2,3]}`);
await delay(1);
expect(dataSource.personCriterion).toMatchObject(
new PersonCriteria({
name: "bob2",
personIds: [1, 2, 3],
})
);

router.push("/?personCriterion=");
await delay(1);
expect(dataSource.personCriterion).toMatchObject(new PersonCriteria({}));
});
});

test("does not put values on new route when changing routes", async () => {
let changeBoundValue: Function;
var router = createRouter({
Expand Down
Loading

0 comments on commit d63d649

Please sign in to comment.